diff --git a/.github/workflows/build-release-zip.yml b/.github/workflows/build-release-zip.yml index e0738668c..c8f139de0 100644 --- a/.github/workflows/build-release-zip.yml +++ b/.github/workflows/build-release-zip.yml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: .nvmrc - name: npm install and build @@ -22,7 +22,7 @@ jobs: composer install --no-dev npm run archive - name: Upload the ZIP file as an artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ github.event.repository.name }} path: release diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9f6fec6ce..a75908f2c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index efc16a857..5a6223b20 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - develop + jobs: cypress: name: ${{ matrix.core.name }} @@ -17,10 +18,10 @@ jobs: core: - {name: 'WP latest', version: 'latest'} - {name: 'WP minimum', version: 'WordPress/WordPress#6.1'} - - {name: 'WP trunk', version: 'WordPress/WordPress#master'} + # - {name: 'WP trunk', version: 'WordPress/WordPress#master'} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install dependencies run: npm install @@ -51,7 +52,7 @@ jobs: cat ./tests/cypress/reports/mochawesome.md >> $GITHUB_STEP_SUMMARY - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: cypress-artifact-classifai diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 070280083..bbb2ff8e8 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,9 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Dependency Review - uses: actions/dependency-review-action@v3 + uses: actions/dependency-review-action@v4 with: license-check: true vulnerability-check: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2c18859a8..21eabce03 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup node v16 and npm cache - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: .nvmrc cache: npm @@ -32,7 +32,7 @@ jobs: - name: Get updated JS files id: changed-files - uses: tj-actions/changed-files@v41 + uses: tj-actions/changed-files@v42 with: files: | **/*.js @@ -52,14 +52,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set standard 10up cache directories run: | composer config -g cache-dir "${{ env.COMPOSER_CACHE }}" - name: Prepare composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE }} key: composer-${{ env.COMPOSER_VERSION }}-${{ hashFiles('**/composer.lock') }} @@ -78,7 +78,7 @@ jobs: - name: Get updated PHP files id: changed-files - uses: tj-actions/changed-files@v41 + uses: tj-actions/changed-files@v42 with: files: | **/*.php @@ -97,7 +97,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: VIPCS check uses: 10up/wpcs-action@stable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3cfeb4000..03e5b4d17 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: .nvmrc diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index cb43f76b2..c71c849fc 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -9,11 +9,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Node.js 16.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 16.x + node-version-file: .nvmrc - name: npm install and build run: | npm install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b73ddbcb..7e1066fd6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,14 +27,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v4 - name: Set standard 10up cache directories run: | composer config -g cache-dir "${{ env.COMPOSER_CACHE }}" - name: Prepare composer cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE }} key: composer-${{ env.COMPOSER_VERSION }}-${{ hashFiles('**/composer.lock') }} @@ -44,7 +44,7 @@ jobs: - uses: getong/mariadb-action@v1.1 - name: Set PHP version - uses: shivammathur/setup-php@2.17.0 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: none diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index cf0c46b93..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no-install lint-staged diff --git a/.wp-env.json b/.wp-env.json index 4b8b9bcb5..d3be216b9 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,5 +1,4 @@ { - "core": "WordPress/WordPress#6.1", "plugins": [".", "./tests/test-plugin", "https://downloads.wordpress.org/plugin/classic-editor.zip"], "env": { "tests": { diff --git a/autoload.php b/autoload.php deleted file mode 100644 index 5ea902fbf..000000000 --- a/autoload.php +++ /dev/null @@ -1,152 +0,0 @@ -prefixes[ $prefix ] ) === false ) { - $this->prefixes[ $prefix ] = array(); - } - - // retain the base directory for the namespace prefix - if ( $prepend ) { - array_unshift( $this->prefixes[ $prefix ], $base_dir ); - } else { - array_push( $this->prefixes[ $prefix ], $base_dir ); - } - } - - /** - * Loads the class file for a given class name. - * - * @param string $classname The fully-qualified class name. - * @return mixed The mapped file name on success, or boolean false on - * failure. - */ - public function load_class( $classname ) { - // the current namespace prefix - $prefix = $classname; - - // work backwards through the namespace names of the fully-qualified - // class name to find a mapped file name - while ( false !== $pos = strrpos( $prefix, '\\' ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition, WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition - - // retain the trailing namespace separator in the prefix - $prefix = substr( $classname, 0, $pos + 1 ); - - // the rest is the relative class name - $relative_class = substr( $classname, $pos + 1 ); - - // try to load a mapped file for the prefix and relative class - $mapped_file = $this->load_mapped_file( $prefix, $relative_class ); - if ( $mapped_file ) { - return $mapped_file; - } - - // remove the trailing namespace separator for the next iteration - // of strrpos() - $prefix = rtrim( $prefix, '\\' ); - } - - // never found a mapped file - return false; - } - - /** - * Load the mapped file for a namespace prefix and relative class. - * - * @param string $prefix The namespace prefix. - * @param string $relative_class The relative class name. - * @return mixed Boolean false if no mapped file can be loaded, or the - * name of the mapped file that was loaded. - */ - protected function load_mapped_file( $prefix, $relative_class ) { - // are there any base directories for this namespace prefix? - if ( isset( $this->prefixes[ $prefix ] ) === false ) { - return false; - } - - // look through base directories for this namespace prefix - foreach ( $this->prefixes[ $prefix ] as $base_dir ) { - - // replace the namespace prefix with the base directory, - // replace namespace separators with directory separators - // in the relative class name, append with .php - $file = $base_dir . str_replace( '\\', '/', $relative_class ) . '.php'; - - // if the mapped file exists, require it - if ( $this->require_file( $file ) ) { - // yes, we're done - return $file; - } - } - - // never found it - return false; - } - - /** - * If a file exists, require it from the file system. - * - * @param string $file The file to require. - * @return bool True if the file exists, false if not. - */ - protected function require_file( $file ) { - if ( file_exists( $file ) ) { - require $file; - return true; - } - return false; - } -} - -// instantiate the loader -$classifai_loader = new \Classifai\Psr4AutoloaderClass(); - -// register the autoloader -$classifai_loader->register(); - -// register the base directories for the namespace prefix -$classifai_loader->add_namespace( 'Classifai', __DIR__ . '/includes/Classifai' ); - -require_once __DIR__ . '/includes/Classifai/Helpers.php'; -require_once __DIR__ . '/includes/Classifai/Blocks.php'; diff --git a/classifai.php b/classifai.php index 35d41d977..79447d8a6 100644 --- a/classifai.php +++ b/classifai.php @@ -59,13 +59,14 @@ function () { } /** - * Small wrapper around PHP's define function. The defined constant is - * ignored if it has already been defined. This allows the - * config.local.php to override any constant in config.php. + * Small wrapper around PHP's define function. * - * @param string $name The constant name - * @param mixed $value The constant value - * @return void + * The defined constant is ignored if it has already + * been defined. This allows these constants to be + * overridden. + * + * @param string $name The constant name. + * @param mixed $value The constant value. */ function classifai_define( $name, $value ) { if ( ! defined( $name ) ) { @@ -73,39 +74,17 @@ function classifai_define( $name, $value ) { } } -if ( file_exists( __DIR__ . '/config.test.php' ) && defined( 'PHPUNIT_RUNNER' ) ) { - require_once __DIR__ . '/config.test.php'; -} - -if ( file_exists( __DIR__ . '/config.local.php' ) ) { - require_once __DIR__ . '/config.local.php'; -} - require_once __DIR__ . '/config.php'; -classifai_define( 'CLASSIFAI_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); /** - * Loads the CLASSIFAI PHP autoloader if possible. + * Loads the autoloader if possible. * * @return bool True or false if autoloading was successful. */ function classifai_autoload() { - if ( classifai_can_autoload() ) { - require_once classifai_autoloader(); + if ( file_exists( CLASSIFAI_PLUGIN_DIR . '/vendor/autoload.php' ) ) { + require_once CLASSIFAI_PLUGIN_DIR . '/vendor/autoload.php'; - return true; - } else { - return false; - } -} - -/** - * In server mode we can autoload if autoloader file exists. For - * test environments we prevent autoloading of the plugin to prevent - * global pollution and for better performance. - */ -function classifai_can_autoload() { - if ( file_exists( classifai_autoloader() ) ) { return true; } else { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log @@ -116,32 +95,19 @@ function classifai_can_autoload() { } } -/** - * Default is Composer's autoloader - */ -function classifai_autoloader() { - if ( file_exists( CLASSIFAI_PLUGIN_DIR . '/vendor/autoload.php' ) ) { - return CLASSIFAI_PLUGIN_DIR . '/vendor/autoload.php'; - } else { - return CLASSIFAI_PLUGIN_DIR . '/autoload.php'; - } -} - /** * Gets the installation message error. * - * This was put in a function specifically because it's used both in WP-CLI and within an admin notice if not using - * WP-CLI. + * Used both in a WP-CLI context and within an admin notice. * * @return string */ function get_error_install_message() { - return esc_html__( 'Error: Please run $ composer install in the classifai plugin directory.', 'classifai' ); + return esc_html__( 'Error: Please run $ composer install in the ClassifAI plugin directory.', 'classifai' ); } /** - * Plugin code entry point. Singleton instance is used to maintain a common single - * instance of the plugin throughout the current request's lifecycle. + * Plugin code entry point. * * If autoloading failed an admin notice is shown and logged to * the PHP error_log. @@ -167,7 +133,6 @@ function classifai_autorun() { } } - /** * Generate a notice if autoload fails. */ @@ -176,9 +141,8 @@ function classifai_autoload_notice() { error_log( get_error_install_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } - /** - * Register an activation hook that we can hook into. + * Run functionality on plugin activation. */ function classifai_activation() { set_transient( 'classifai_activation_notice', 'classifai', HOUR_IN_SECONDS ); diff --git a/composer.json b/composer.json index b5a51ac7e..40a11b254 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ }, "files": [ "includes/Classifai/Helpers.php", - "includes/Classifai/Blocks.php" + "includes/Classifai/Blocks.php", + "includes/Classifai/Providers/Watson/Helpers.php" ] }, "require-dev": { diff --git a/composer.lock b/composer.lock index 62cec493d..fa4caee7d 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "90d087e988ff194065333d16bc5cf649872d9cdb" + "reference": "b66d11b7479109ab547f9405b97205640b17d385" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/90d087e988ff194065333d16bc5cf649872d9cdb", - "reference": "90d087e988ff194065333d16bc5cf649872d9cdb", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", + "reference": "b66d11b7479109ab547f9405b97205640b17d385", "shasum": "" }, "require": { @@ -29,7 +29,7 @@ "phpstan/phpstan": "^0.12.55", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "default-branch": true, "type": "library", @@ -65,7 +65,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.3.6" + "source": "https://github.com/composer/ca-bundle/tree/1.4.0" }, "funding": [ { @@ -81,7 +81,7 @@ "type": "tidelift" } ], - "time": "2023-06-06T12:02:59+00:00" + "time": "2023-12-18T12:05:55+00:00" }, { "name": "ua-parser/uap-php", @@ -377,25 +377,25 @@ "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "12be2483e1f0e850b353e26869e4e6c038459501" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/12be2483e1f0e850b353e26869e4e6c038459501", + "reference": "12be2483e1f0e850b353e26869e4e6c038459501", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^9 || ^12", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", @@ -423,7 +423,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/1.5.x" }, "funding": [ { @@ -439,7 +439,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2023-12-09T14:16:53+00:00" }, { "name": "myclabs/deep-copy", @@ -447,12 +447,12 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "f6f48cfecf52ab791fe18cc1b11d6345512dc4b8" + "reference": "202aaf6b7c2e1e0a622b0298e9f3f537e4d84018" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/f6f48cfecf52ab791fe18cc1b11d6345512dc4b8", - "reference": "f6f48cfecf52ab791fe18cc1b11d6345512dc4b8", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/202aaf6b7c2e1e0a622b0298e9f3f537e4d84018", + "reference": "202aaf6b7c2e1e0a622b0298e9f3f537e4d84018", "shasum": "" }, "require": { @@ -500,29 +500,31 @@ "type": "tidelift" } ], - "time": "2023-07-30T10:01:33+00:00" + "time": "2023-11-01T08:01:43+00:00" }, { "name": "nikic/php-parser", - "version": "4.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "ce019e9ad711e31ee87c2c4c72e538b5240970c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ce019e9ad711e31ee87c2c4c72e538b5240970c3", + "reference": "ce019e9ad711e31ee87c2c4c72e538b5240970c3", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "default-branch": true, "bin": [ @@ -531,7 +533,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -555,9 +557,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/master" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2024-01-14T09:02:54+00:00" }, { "name": "phar-io/manifest", @@ -937,12 +939,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "7ce595672008088280161470266240d39a4025a3" + "reference": "26dcb893d86fbe90ab2a8abd7b08a3fda3602237" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/7ce595672008088280161470266240d39a4025a3", - "reference": "7ce595672008088280161470266240d39a4025a3", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/26dcb893d86fbe90ab2a8abd7b08a3fda3602237", + "reference": "26dcb893d86fbe90ab2a8abd7b08a3fda3602237", "shasum": "" }, "require": { @@ -1018,7 +1020,7 @@ "type": "open_collective" } ], - "time": "2023-12-11T10:09:05+00:00" + "time": "2024-01-15T05:03:54+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1026,19 +1028,19 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "a74e6341296fe533cb69c8c94193afc7537443ec" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/a74e6341296fe533cb69c8c94193afc7537443ec", - "reference": "a74e6341296fe533cb69c8c94193afc7537443ec", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -1088,7 +1090,7 @@ "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" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -1096,7 +1098,7 @@ "type": "github" } ], - "time": "2023-08-21T05:57:35+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1345,12 +1347,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6c24b9dd8390a031a5490a2443fff1958522b49a" + "reference": "4c1997c21fb0e29198b7b83be49d460df2571d79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6c24b9dd8390a031a5490a2443fff1958522b49a", - "reference": "6c24b9dd8390a031a5490a2443fff1958522b49a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4c1997c21fb0e29198b7b83be49d460df2571d79", + "reference": "4c1997c21fb0e29198b7b83be49d460df2571d79", "shasum": "" }, "require": { @@ -1365,7 +1367,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-code-coverage": "^9.2.28", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -1440,7 +1442,7 @@ "type": "tidelift" } ], - "time": "2023-08-21T05:56:05+00:00" + "time": "2024-01-21T09:34:47+00:00" }, { "name": "sebastian/cli-parser", @@ -1685,20 +1687,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -1730,7 +1732,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -1738,7 +1740,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -2012,20 +2014,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2057,7 +2059,7 @@ "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.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -2065,7 +2067,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -2471,12 +2473,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "88d3cec355a252c8fb6a338c420960bf1b0d5679" + "reference": "b03d10fc5a68504e3ea28fc84651b92cb0252fd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/88d3cec355a252c8fb6a338c420960bf1b0d5679", - "reference": "88d3cec355a252c8fb6a338c420960bf1b0d5679", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/b03d10fc5a68504e3ea28fc84651b92cb0252fd9", + "reference": "b03d10fc5a68504e3ea28fc84651b92cb0252fd9", "shasum": "" }, "require": { @@ -2486,7 +2488,7 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "default-branch": true, "bin": [ @@ -2544,20 +2546,20 @@ "type": "open_collective" } ], - "time": "2023-12-14T14:17:01+00:00" + "time": "2024-01-22T02:36:17+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -2586,7 +2588,7 @@ "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.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -2594,7 +2596,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -2668,12 +2670,12 @@ "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212" + "reference": "f07cf7b2ea73c3de1f72cf115e3cd446c8ad2713" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/224e4a1329c03d8bad520e3fc4ec980034a4b212", - "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/f07cf7b2ea73c3de1f72cf115e3cd446c8ad2713", + "reference": "f07cf7b2ea73c3de1f72cf115e3cd446c8ad2713", "shasum": "" }, "require": { @@ -2681,7 +2683,9 @@ "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "require-dev": { - "yoast/yoastcs": "^2.3.0" + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "yoast/yoastcs": "^3.0.0" }, "type": "library", "extra": { @@ -2718,9 +2722,10 @@ ], "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": "2023-08-19T14:25:08+00:00" + "time": "2023-12-14T21:29:51+00:00" } ], "aliases": [], diff --git a/config.php b/config.php index 4050f7991..2c3c36e0f 100644 --- a/config.php +++ b/config.php @@ -1,20 +1,18 @@ chat_gpt = new ChatGPT( false ); - $this->embeddings = new Embeddings( false ); - $this->text_to_speech = new TextToSpeech( false ); - $this->ibm_watson_nlu = new NLU( false ); + $this->language_processing_features = [ + new Classification(), + new ExcerptGeneration(), + new TextToSpeech(), + ]; + + foreach ( $this->language_processing_features as $feature ) { + if ( ! $feature->is_feature_enabled() ) { + continue; + } - $embeddings_post_types = []; - $nlu_post_types = []; - $text_to_speech_post_types = []; - $chat_gpt_post_types = []; + $settings = $feature->get_settings(); - // Set up the NLU post types if the feature is enabled. Otherwise clear. - if ( - $this->ibm_watson_nlu && - $this->ibm_watson_nlu->is_feature_enabled( 'content_classification' ) - ) { - $nlu_post_types = get_supported_post_types(); - } else { - $this->ibm_watson_nlu = null; - } + if ( ! isset( $settings['post_types'] ) ) { + continue; + } - // Set up the NLU post types if the feature is enabled. Otherwise clear. - if ( - $this->text_to_speech && - $this->text_to_speech->is_feature_enabled( 'content_classification' ) - ) { - $text_to_speech_post_types = get_tts_supported_post_types(); - } else { - $this->text_to_speech = null; + foreach ( $settings['post_types'] as $key => $post_type ) { + add_filter( "bulk_actions-edit-$post_type", [ $this, 'register_language_processing_actions' ] ); + add_filter( "handle_bulk_actions-edit-$post_type", [ $this, 'language_processing_actions_handler' ], 10, 3 ); + + if ( is_post_type_hierarchical( $post_type ) ) { + add_filter( 'page_row_actions', [ $this, 'register_language_processing_row_action' ], 10, 2 ); + } else { + add_filter( 'post_row_actions', [ $this, 'register_language_processing_row_action' ], 10, 2 ); + } + } } + } + + /** + * Register Language Processing bulk actions. + * + * @param array $bulk_actions Current bulk actions. + * @return array + */ + public function register_language_processing_actions( array $bulk_actions ): array { + foreach ( $this->language_processing_features as $feature ) { + if ( ! $feature->is_feature_enabled() ) { + continue; + } + + $bulk_actions[ $feature::ID ] = $feature->get_label(); + + switch ( $feature::ID ) { + case Classification::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Classify', 'classifai' ); + break; + + case ExcerptGeneration::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Generate Excerpt', 'classifai' ); + break; - // Set up the save post handler if we have any post types. - if ( ! empty( $nlu_post_types ) || ! empty( $text_to_speech_post_types ) ) { - $this->save_post_handler = new SavePostHandler(); + case TextToSpeech::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Generate audio (text to speech)', 'classifai' ); + break; + } } - // Set up the ChatGPT post types if the feature is enabled. Otherwise clear our handler. + return $bulk_actions; + } + + /** + * Handle language processing bulk actions. + * + * @param string $redirect_to Redirect URL after bulk actions. + * @param string $doaction Action ID. + * @param array $post_ids Post ids to apply bulk actions to. + * @return string + */ + public function language_processing_actions_handler( string $redirect_to, string $doaction, array $post_ids ): string { + $feature_ids = array_map( + function ( $feature ) { + return $feature::ID; + }, + $this->language_processing_features + ); + if ( - $this->chat_gpt && - $this->chat_gpt->is_feature_enabled( 'excerpt_generation' ) + empty( $post_ids ) || + ! in_array( $doaction, $feature_ids, true ) ) { - $chat_gpt_post_types = array_keys( get_post_types_for_language_settings() ); - } else { - $this->chat_gpt = null; + return $redirect_to; } - // Set up the embeddings post types if the feature is enabled. Otherwise clear our embeddings handler. - if ( $this->embeddings && $this->embeddings->is_feature_enabled( 'classification' ) ) { - $embeddings_post_types = $this->embeddings->supported_post_types(); - } else { - $this->embeddings = null; - } + foreach ( $post_ids as $post_id ) { + switch ( $doaction ) { + case Classification::ID: + ( new Classification() )->run( $post_id ); + $action = $doaction; + break; - // Merge our post types together and make them unique. - $post_types = array_unique( array_merge( $chat_gpt_post_types, $embeddings_post_types, $nlu_post_types, $text_to_speech_post_types ) ); + case ExcerptGeneration::ID: + $excerpt = ( new ExcerptGeneration() )->run( $post_id, 'excerpt' ); + $action = $doaction; - if ( empty( $post_types ) ) { - return; - } + if ( ! is_wp_error( $excerpt ) ) { + wp_update_post( + [ + 'ID' => $post_id, + 'post_excerpt' => $excerpt, + ] + ); + } + break; - foreach ( $post_types as $post_type ) { - add_filter( "bulk_actions-edit-$post_type", [ $this, 'register_bulk_actions' ] ); - add_filter( "handle_bulk_actions-edit-$post_type", [ $this, 'bulk_action_handler' ], 10, 3 ); + case TextToSpeech::ID: + $tts = new TextToSpeech(); + $results = $tts->run( $post_id, 'synthesize' ); - if ( is_post_type_hierarchical( $post_type ) ) { - add_filter( 'page_row_actions', [ $this, 'register_row_action' ], 10, 2 ); - } else { - add_filter( 'post_row_actions', [ $this, 'register_row_action' ], 10, 2 ); + if ( $results && ! is_wp_error( $results ) ) { + $tts->save( $results, $post_id ); + } + $action = $doaction; + break; } } - } - /** - * Register bulk actions for the Computer Vision provider. - */ - public function register_image_processing_hooks() { - $this->computer_vision = new ComputerVision( false ); - $this->whisper = new Whisper( false ); + $args_to_remove = array_map( + function ( $feature ) { + return "bulk_{$feature}"; + }, + $feature_ids + ); - add_filter( 'bulk_actions-upload', [ $this, 'register_media_bulk_actions' ] ); - add_filter( 'handle_bulk_actions-upload', [ $this, 'media_bulk_action_handler' ], 10, 3 ); - add_filter( 'media_row_actions', [ $this, 'register_media_row_action' ], 10, 2 ); + $redirect_to = remove_query_arg( $args_to_remove, $redirect_to ); + $redirect_to = add_query_arg( rawurlencode( "bulk_{$action}" ), count( $post_ids ), $redirect_to ); + + return esc_url_raw( $redirect_to ); } /** - * Register language processing bulk actions. - * - * @param array $bulk_actions Current bulk actions. + * Register Language Processing row actions. * + * @param array $actions Current row actions. + * @param \WP_Post $post Post object. * @return array */ - public function register_bulk_actions( $bulk_actions ) { - $nlu_post_types = get_supported_post_types(); - - if ( - ( - is_a( $this->ibm_watson_nlu, '\Classifai\Providers\Watson\NLU' ) && - $this->ibm_watson_nlu->is_feature_enabled( 'content_classification' ) && - ! empty( $nlu_post_types ) - ) || - ( - is_a( $this->embeddings, '\Classifai\Providers\OpenAI\Embeddings' ) && - $this->embeddings->is_feature_enabled( 'classification' ) && - ! empty( $this->embeddings->supported_post_types() ) - ) - ) { - $bulk_actions['classify'] = __( 'Classify', 'classifai' ); - } + public function register_language_processing_row_action( array $actions, \WP_Post $post ): array { + foreach ( $this->language_processing_features as $feature ) { + if ( ! $feature->is_feature_enabled() ) { + continue; + } - if ( - is_a( $this->chat_gpt, '\Classifai\Providers\OpenAI\ChatGPT' ) && - in_array( get_current_screen()->post_type, array_keys( get_post_types_for_language_settings() ), true ) && - $this->chat_gpt->is_feature_enabled( 'excerpt_generation' ) - ) { - $bulk_actions['generate_excerpt'] = __( 'Generate excerpt', 'classifai' ); + switch ( $feature::ID ) { + case Classification::ID: + $actions[ Classification::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'edit.php?action=%s&ids=%d&post_type=%s', Classification::ID, $post->ID, $post->post_type ) ), 'bulk-posts' ) ), + esc_html__( 'Classify', 'classifai' ) + ); + break; + + case ExcerptGeneration::ID: + $actions[ ExcerptGeneration::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'edit.php?action=%s&ids=%d&post_type=%s', ExcerptGeneration::ID, $post->ID, $post->post_type ) ), 'bulk-posts' ) ), + esc_html__( 'Generate excerpt', 'classifai' ) + ); + break; + + case TextToSpeech::ID: + $actions[ TextToSpeech::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'edit.php?action=%s&ids=%d&post_type=%s', TextToSpeech::ID, $post->ID, $post->post_type ) ), 'bulk-posts' ) ), + esc_html__( 'Text to speech', 'classifai' ) + ); + break; + } } - if ( - is_a( $this->text_to_speech, '\Classifai\Providers\Azure\TextToSpeech' ) && - in_array( get_current_screen()->post_type, get_tts_supported_post_types(), true ) && - $this->text_to_speech->is_feature_enabled( 'text_to_speech' ) - ) { - $bulk_actions['text_to_speech'] = __( 'Text to speech', 'classifai' ); - } + return $actions; + } - return $bulk_actions; + /** + * Register Image Processing hooks. + */ + public function register_image_processing_hooks() { + $this->media_processing_features = [ + new DescriptiveTextGenerator(), + new ImageTagsGenerator(), + new ImageCropping(), + new ImageTextExtraction(), + new PDFTextExtraction(), + new AudioTranscriptsGeneration(), + ]; + + add_filter( 'bulk_actions-upload', [ $this, 'register_media_processing_media_bulk_actions' ] ); + add_filter( 'handle_bulk_actions-upload', [ $this, 'media_processing_bulk_action_handler' ], 10, 3 ); + add_filter( 'media_row_actions', [ $this, 'register_media_processing_row_action' ], 10, 2 ); } /** - * Register Classifai media bulk actions. + * Register Image Processing bulk actions. * * @param array $bulk_actions Current bulk actions. - * * @return array */ - public function register_media_bulk_actions( $bulk_actions ) { - $whisper_enabled = $this->whisper->is_feature_enabled( 'speech_to_text' ); + public function register_media_processing_media_bulk_actions( array $bulk_actions ): array { + foreach ( $this->media_processing_features as $feature ) { + if ( ! $feature->is_feature_enabled() ) { + continue; + } - if ( - $this->computer_vision->is_feature_enabled( 'image_tagging' ) || - $this->computer_vision->is_feature_enabled( 'image_captions' ) - ) { - $bulk_actions['scan_image'] = __( 'Scan image', 'classifai' ); - } + $bulk_actions[ $feature::ID ] = $feature->get_label(); - if ( $this->computer_vision && $this->computer_vision->is_feature_enabled( 'smart_cropping' ) ) { - $bulk_actions['smart_crop'] = __( 'Smart crop', 'classifai' ); - } + switch ( $feature::ID ) { + case DescriptiveTextGenerator::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Generate descriptive text', 'classifai' ); + break; + + case ImageTagsGenerator::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Generate image tags', 'classifai' ); + break; + + case ImageCropping::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Crop image', 'classifai' ); + break; - if ( ! is_wp_error( $whisper_enabled ) ) { - $bulk_actions['transcribe'] = __( 'Transcribe audio', 'classifai' ); + case ImageTextExtraction::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Extract text from images', 'classifai' ); + break; + + case PDFTextExtraction::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Extract text from PDFs', 'classifai' ); + break; + + case AudioTranscriptsGeneration::ID: + $bulk_actions[ $feature::ID ] = esc_html__( 'Transcribe audio', 'classifai' ); + break; + } } return $bulk_actions; } /** - * Handle language processing bulk actions. - * - * @param string $redirect_to Redirect URL after bulk actions. - * @param string $doaction Action ID. - * @param array $post_ids Post ids to apply bulk actions to. + * Handle Image Processing bulk actions. * + * @param string $redirect_to Redirect URL after bulk actions. + * @param string $doaction Action ID. + * @param array $attachment_ids Attachment ids to apply bulk actions to. * @return string */ - public function bulk_action_handler( $redirect_to, $doaction, $post_ids ) { + public function media_processing_bulk_action_handler( string $redirect_to, string $doaction, array $attachment_ids ): string { + $feature_ids = array_map( + function ( $feature ) { + return $feature::ID; + }, + $this->media_processing_features + ); + if ( - empty( $post_ids ) || - ! in_array( $doaction, [ 'classify', 'generate_excerpt', 'text_to_speech' ], true ) + empty( $attachment_ids ) || + ! in_array( $doaction, $feature_ids, true ) ) { return $redirect_to; } - $action = ''; + foreach ( $attachment_ids as $attachment_id ) { + $current_meta = wp_get_attachment_metadata( $attachment_id ); - foreach ( $post_ids as $post_id ) { - if ( 'classify' === $doaction ) { - // Handle NLU classification. - if ( - is_a( $this->ibm_watson_nlu, '\Classifai\Providers\Watson\NLU' ) && - is_a( $this->save_post_handler, '\Classifai\Admin\SavePostHandler' ) - ) { - $action = 'classified'; - $this->save_post_handler->classify( $post_id ); - } + switch ( $doaction ) { + case DescriptiveTextGenerator::ID: + if ( wp_attachment_is_image( $attachment_id ) ) { + $desc_text = new DescriptiveTextGenerator(); + $desc_text_result = $desc_text->run( $attachment_id, 'descriptive_text' ); - // Handle OpenAI Embeddings classification. - if ( is_a( $this->embeddings, '\Classifai\Providers\OpenAI\Embeddings' ) ) { - $action = 'classified'; - $this->embeddings->generate_embeddings_for_post( $post_id ); - } - } + if ( $desc_text_result && ! is_wp_error( $desc_text_result ) ) { + $desc_text->save( $desc_text_result, $attachment_id ); + } + } + break; - if ( 'generate_excerpt' === $doaction ) { - if ( is_a( $this->chat_gpt, '\Classifai\Providers\OpenAI\ChatGPT' ) ) { - $action = 'excerpt_generated'; - $excerpt = $this->chat_gpt->generate_excerpt( $post_id ); - if ( ! is_wp_error( $excerpt ) ) { - wp_update_post( - [ - 'ID' => $post_id, - 'post_excerpt' => $excerpt, - ] - ); + case ImageTagsGenerator::ID: + if ( wp_attachment_is_image( $attachment_id ) ) { + $image_tags = new ImageTagsGenerator(); + $tags_result = $image_tags->run( $attachment_id, 'tags' ); + + if ( ! empty( $tags_result ) && ! is_wp_error( $tags_result ) ) { + $image_tags->save( $tags_result, $attachment_id ); + } } - } - } + break; + + case ImageCropping::ID: + if ( wp_attachment_is_image( $attachment_id ) ) { + $crop = new ImageCropping(); + $crop_result = $crop->run( $attachment_id, 'crop', $current_meta ); + if ( ! empty( $crop_result ) && ! is_wp_error( $crop_result ) ) { + $ocr_meta = $crop->save( $crop_result, $attachment_id ); + wp_update_attachment_metadata( $attachment_id, $ocr_meta ); + } + } + break; + + case ImageTextExtraction::ID: + if ( wp_attachment_is_image( $attachment_id ) ) { + $ocr = new ImageTextExtraction(); + $ocr_result = $ocr->run( $attachment_id, 'ocr' ); + if ( $ocr_result && ! is_wp_error( $ocr_result ) ) { + $ocr->save( $ocr_result, $attachment_id ); + } + } + break; - if ( 'text_to_speech' === $doaction ) { - // Handle Azure Text to Speech generation. - if ( - is_a( $this->text_to_speech, '\Classifai\Providers\Azure\TextToSpeech' ) && - is_a( $this->save_post_handler, '\Classifai\Admin\SavePostHandler' ) - ) { - $action = 'text_to_speech'; - $this->save_post_handler->synthesize_speech( $post_id ); - } + case PDFTextExtraction::ID: + if ( attachment_is_pdf( $attachment_id ) ) { + ( new PDFTextExtraction() )->run( $attachment_id, 'read_pdf' ); + } + break; + + case AudioTranscriptsGeneration::ID: + if ( wp_attachment_is( 'audio', $attachment_id ) ) { + ( new AudioTranscriptsGeneration() )->run( $attachment_id, 'transcript' ); + } + break; } } - $redirect_to = remove_query_arg( [ 'bulk_classified', 'bulk_excerpt_generated', 'bulk_text_to_speech', 'bulk_scanned', 'bulk_cropped', 'bulk_transcribed' ], $redirect_to ); - $redirect_to = add_query_arg( rawurlencode( "bulk_{$action}" ), count( $post_ids ), $redirect_to ); + $args_to_remove = array_map( + function ( $feature ) { + return "bulk_{$feature}"; + }, + $feature_ids + ); + + $redirect_to = remove_query_arg( $args_to_remove, $redirect_to ); + $redirect_to = add_query_arg( rawurlencode( "bulk_{$doaction}" ), count( $attachment_ids ), $redirect_to ); return esc_url_raw( $redirect_to ); } /** - * Handle media bulk actions. + * Register Image Processing row actions. * - * @param string $redirect_to Redirect URL after bulk actions. - * @param string $doaction Action ID. - * @param array $attachment_ids Attachment ids to apply bulk actions to. - * - * @return string + * @param array $actions An array of action links for each attachment. + * @param \WP_Post $post WP_Post object for the current attachment. + * @return array */ - public function media_bulk_action_handler( $redirect_to, $doaction, $attachment_ids ) { - if ( - empty( $attachment_ids ) || - ! in_array( $doaction, [ 'scan_image', 'smart_crop', 'transcribe' ], true ) - ) { - return $redirect_to; + public function register_media_processing_row_action( array $actions, \WP_Post $post ): array { + if ( attachment_is_pdf( $post ) && ( new PDFTextExtraction() )->is_feature_enabled() ) { + $actions[ PDFTextExtraction::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=%s&ids=%d&post_type=%s', PDFTextExtraction::ID, $post->ID, $post->post_type ) ), 'bulk-media' ) ), + esc_html__( 'Extract text from PDF', 'classifai' ) + ); } - $action = ''; + if ( wp_attachment_is( 'image' ) ) { + if ( ( new DescriptiveTextGenerator() )->is_feature_enabled() ) { + $actions[ DescriptiveTextGenerator::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=%s&ids=%d&post_type=%s', DescriptiveTextGenerator::ID, $post->ID, $post->post_type ) ), 'bulk-media' ) ), + esc_html__( 'Generate descriptive text', 'classifai' ) + ); + } - foreach ( $attachment_ids as $attachment_id ) { - if ( 'transcribe' === $doaction ) { - $action = 'transcribed'; - $this->whisper->transcribe_audio( $attachment_id ); - continue; + if ( ( new ImageTagsGenerator() )->is_feature_enabled() ) { + $actions[ ImageTagsGenerator::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=%s&ids=%d&post_type=%s', ImageTagsGenerator::ID, $post->ID, $post->post_type ) ), 'bulk-media' ) ), + esc_html__( 'Generate image tags', 'classifai' ) + ); } - $current_meta = wp_get_attachment_metadata( $attachment_id ); + if ( ( new ImageCropping() )->is_feature_enabled() ) { + $actions[ ImageCropping::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=%s&ids=%d&post_type=%s', ImageCropping::ID, $post->ID, $post->post_type ) ), 'bulk-media' ) ), + esc_html__( 'Crop image', 'classifai' ) + ); + } - if ( 'smart_crop' === $doaction ) { - $action = 'cropped'; - $this->computer_vision->smart_crop_image( $current_meta, $attachment_id ); - } elseif ( 'scan_image' === $doaction ) { - $action = 'scanned'; - $this->computer_vision->generate_image_alt_tags( $current_meta, $attachment_id ); + if ( ( new ImageTextExtraction() )->is_feature_enabled() ) { + $actions[ ImageTextExtraction::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=%s&ids=%d&post_type=%s', ImageTextExtraction::ID, $post->ID, $post->post_type ) ), 'bulk-media' ) ), + esc_html__( 'Extract text from image', 'classifai' ) + ); } } - $redirect_to = remove_query_arg( [ 'bulk_classified', 'bulk_text_to_speech', 'bulk_scanned', 'bulk_cropped', 'bulk_transcribed' ], $redirect_to ); - $redirect_to = add_query_arg( rawurlencode( "bulk_{$action}" ), count( $attachment_ids ), $redirect_to ); + if ( wp_attachment_is( 'audio', $post ) && ( new AudioTranscriptsGeneration() )->is_feature_enabled() ) { + $actions[ AudioTranscriptsGeneration::ID ] = sprintf( + '%s', + esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=%s&ids=%d&post_type=%s', AudioTranscriptsGeneration::ID, $post->ID, $post->post_type ) ), 'bulk-media' ) ), + esc_html__( 'Transcribe audio', 'classifai' ) + ); + } - return esc_url_raw( $redirect_to ); + return $actions; } /** * Display an admin notice after bulk updates. */ public function bulk_action_admin_notice() { + $post_count = 0; + $action = ''; + $post_type = ! empty( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : 'post'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $all_feature_ids = array_map( + function ( $feature ) { + return $feature::ID; + }, + array_merge( $this->language_processing_features, $this->media_processing_features ) + ); - $classified = ! empty( $_GET['bulk_classified'] ) ? intval( wp_unslash( $_GET['bulk_classified'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $excerpts = ! empty( $_GET['bulk_excerpt_generated'] ) ? intval( wp_unslash( $_GET['bulk_excerpt_generated'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $text_to_speech = ! empty( $_GET['bulk_text_to_speech'] ) ? intval( wp_unslash( $_GET['bulk_text_to_speech'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $post_type = ! empty( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : 'post'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $scanned = ! empty( $_GET['bulk_scanned'] ) ? intval( wp_unslash( $_GET['bulk_scanned'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $cropped = ! empty( $_GET['bulk_cropped'] ) ? intval( wp_unslash( $_GET['bulk_cropped'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $transcribed = ! empty( $_GET['bulk_transcribed'] ) ? intval( wp_unslash( $_GET['bulk_transcribed'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + foreach ( $all_feature_ids as $feature_id ) { + $post_count = ! empty( $_GET[ "bulk_{$feature_id}" ] ) ? intval( wp_unslash( $_GET[ "bulk_{$feature_id}" ] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! $classified && ! $excerpts && ! $text_to_speech && ! $scanned && ! $cropped && ! $transcribed ) { + if ( $post_count ) { + $action = $feature_id; + break; + } + } + + if ( ! $action ) { return; } - if ( $classified ) { - $classified_posts_count = $classified; - $post_type = $post_type; - $action = __( 'Classified', 'classifai' ); - } elseif ( $excerpts ) { - $classified_posts_count = $excerpts; - $post_type = $post_type; - $action = __( 'Excerpts generated for', 'classifai' ); - } elseif ( $text_to_speech ) { - $classified_posts_count = $text_to_speech; - $post_type = $post_type; - $action = __( 'Text to speech conversion done for', 'classifai' ); - } elseif ( $scanned ) { - $classified_posts_count = $scanned; - $post_type = 'image'; - $action = __( 'Scanned', 'classifai' ); - } elseif ( $cropped ) { - $classified_posts_count = $cropped; - $post_type = 'image'; - $action = __( 'Cropped', 'classifai' ); - } elseif ( $transcribed ) { - $classified_posts_count = $transcribed; - $post_type = 'audio'; - $action = __( 'Transcribed', 'classifai' ); + switch ( $feature_id ) { + case ExcerptGeneration::ID: + $action_text = __( 'Excerpts generated for', 'classifai' ); + break; + + case TextToSpeech::ID: + $action_text = __( 'Text to speech conversion done for', 'classifai' ); + break; + + case PDFTextExtraction::ID: + $action_text = __( 'PDF Text extraction done for', 'classifai' ); + $post_type = 'file'; + break; + + case DescriptiveTextGenerator::ID: + $action_text = __( 'Alt text generated for', 'classifai' ); + $post_type = 'image'; + break; + + case ImageTagsGenerator::ID: + $action_text = __( 'Tags generated for', 'classifai' ); + $post_type = 'image'; + break; + + case ImageCropping::ID: + $action_text = __( 'Cropping done for', 'classifai' ); + $post_type = 'image'; + break; + + case ImageTextExtraction::ID: + $action_text = __( 'Text extraction done for', 'classifai' ); + $post_type = 'image'; + break; + + case AudioTranscriptsGeneration::ID: + $action_text = __( 'Audio transcribed for', 'classifai' ); + $post_type = 'file'; + break; + + case Classification::ID: + $action_text = __( 'Classification done for', 'classifai' ); + break; } $output = '

'; @@ -393,11 +525,11 @@ public function bulk_action_admin_notice() { _n( '%1$s %2$s %3$s.', '%1$s %2$s %3$ss.', - $classified_posts_count, + $post_count, 'classifai' ), - $action, - $classified_posts_count, + $action_text, + $post_count, $post_type ); $output .= '

'; @@ -413,80 +545,4 @@ public function bulk_action_admin_notice() { ] ); } - - /** - * Register Classifai row action. - * - * @param array $actions Current row actions. - * @param \WP_Post $post Post object. - * - * @return array - */ - public function register_row_action( $actions, $post ) { - $post_types = []; - - if ( is_a( $this->save_post_handler, '\Classifai\Admin\SavePostHandler' ) ) { - $post_types = array_merge( $post_types, get_supported_post_types() ); - } - - if ( is_a( $this->embeddings, '\Classifai\Providers\OpenAI\Embeddings' ) ) { - $post_types = array_merge( $post_types, $this->embeddings->supported_post_types() ); - } - - if ( in_array( $post->post_type, $post_types, true ) ) { - $actions['classify'] = sprintf( - '%s', - esc_url( wp_nonce_url( admin_url( sprintf( 'edit.php?action=classify&ids=%d&post_type=%s', $post->ID, $post->post_type ) ), 'bulk-posts' ) ), - esc_html__( 'Classify', 'classifai' ) - ); - } - - if ( is_a( $this->chat_gpt, '\Classifai\Providers\OpenAI\ChatGPT' ) ) { - if ( in_array( $post->post_type, array_keys( get_post_types_for_language_settings() ), true ) ) { - $actions['generate_excerpt'] = sprintf( - '%s', - esc_url( wp_nonce_url( admin_url( sprintf( 'edit.php?action=generate_excerpt&ids=%d&post_type=%s', $post->ID, $post->post_type ) ), 'bulk-posts' ) ), - esc_html__( 'Generate excerpt', 'classifai' ) - ); - } - } - - if ( is_a( $this->text_to_speech, '\Classifai\Providers\Azure\TextToSpeech' ) && $this->text_to_speech->is_feature_enabled( 'text_to_speech' ) && in_array( $post->post_type, get_tts_supported_post_types(), true ) ) { - $actions['text_to_speech'] = sprintf( - '%s', - esc_url( wp_nonce_url( admin_url( sprintf( 'edit.php?action=text_to_speech&ids=%d&post_type=%s', $post->ID, $post->post_type ) ), 'bulk-posts' ) ), - esc_html__( 'Text to speech', 'classifai' ) - ); - } - - return $actions; - } - - /** - * Register media row actions. - * - * @param array $actions An array of action links for each attachment. - * @param \WP_Post $post WP_Post object for the current attachment. - * @return array - */ - public function register_media_row_action( $actions, $post ) { - $whisper_settings = $this->whisper->get_settings(); - $whisper_enabled = $this->whisper->is_feature_enabled( 'speech_to_text', $post->ID ); - - if ( is_wp_error( $whisper_enabled ) ) { - return $actions; - } - - $transcribe = new Transcribe( $post->ID, $whisper_settings ); - - if ( $transcribe->should_process( $post->ID ) ) { - $actions['transcribe'] = sprintf( - '%s', - esc_url( wp_nonce_url( admin_url( sprintf( 'upload.php?action=transcribe&ids=%d&post_type=%s', $post->ID, $post->post_type ) ), 'bulk-media' ) ), - esc_html__( 'Transcribe', 'classifai' ) - ); - } - - return $actions; - } } diff --git a/includes/Classifai/Admin/DebugInfo.php b/includes/Classifai/Admin/DebugInfo.php index cf9132a60..c023a8ccb 100644 --- a/includes/Classifai/Admin/DebugInfo.php +++ b/includes/Classifai/Admin/DebugInfo.php @@ -18,15 +18,16 @@ class DebugInfo { /** * Checks whether this class's register method should run. * - * @return bool * @since 1.4.0 + * + * @return bool */ - public function can_register() { + public function can_register(): bool { return is_admin(); } /** - * Adds WP hook callbacks. + * Adds hook callbacks. * * @since 1.4.0 */ @@ -38,14 +39,13 @@ public function register() { * Modifies debug information displayed on the WP Site Health screen. * * @see WP_Debug_Data::debug_data - * @filter debug_information + * + * @since 1.4.0 * * @param array $information The full array of site debug information. * @return array Filtered debug information. - * - * @since 1.4.0 */ - public function add_classifai_debug_information( $information ) { + public function add_classifai_debug_information( array $information ): array { $plugin_data = get_plugin_data( CLASSIFAI_PLUGIN ); /** diff --git a/includes/Classifai/Admin/Notifications.php b/includes/Classifai/Admin/Notifications.php index 52219ace7..307d06865 100644 --- a/includes/Classifai/Admin/Notifications.php +++ b/includes/Classifai/Admin/Notifications.php @@ -14,7 +14,7 @@ class Notifications { * * @return bool */ - public function can_register() { + public function can_register(): bool { return is_admin(); } @@ -50,7 +50,8 @@ public function maybe_render_notices() { $needs_setup = get_transient( 'classifai_activation_notice' ); if ( $needs_setup ) { - if ( Onboarding::is_onboarding_completed() ) { + $onboarding = new Onboarding(); + if ( $onboarding->is_onboarding_completed() ) { delete_transient( 'classifai_activation_notice' ); return; } diff --git a/includes/Classifai/Admin/Onboarding.php b/includes/Classifai/Admin/Onboarding.php index 9caab2777..de6813f17 100644 --- a/includes/Classifai/Admin/Onboarding.php +++ b/includes/Classifai/Admin/Onboarding.php @@ -12,6 +12,11 @@ class Onboarding { */ protected $setup_url; + /** + * @var array $features The list of features. + */ + public $features = array(); + /** * Register the actions needed. */ @@ -114,7 +119,7 @@ public function render_setup_page() { -
+
get_features( false ); $onboarding_options = array( 'status' => 'inprogress', ); @@ -184,34 +188,19 @@ public function handle_step_submission() { // Disable unchecked features. $configured_features = $this->get_configured_features(); - $providers = $this->get_providers(); - foreach ( $configured_features as $provider_key => $data ) { - $save_needed = false; - $provider = $providers[ $provider_key ]; - if ( empty( $provider ) ) { - continue; - } - $settings = $provider->get_settings(); + foreach ( $configured_features as $feature_key ) { + $enabled = isset( $enabled_features[ $feature_key ] ); - foreach ( $data as $feature_key ) { - $enabled = isset( $enabled_features[ $provider_key ][ $feature_key ] ); - $keys = explode( '__', $feature_key ); - if ( count( $keys ) > 1 ) { - $enabled = isset( $enabled_features[ $provider_key ][ $keys[0] ][ $keys[1] ] ); - } - - if ( ! $enabled ) { - unset( $settings[ $feature_key ] ); - if ( count( $keys ) > 1 ) { - unset( $settings[ $keys[0] ][ $keys[1] ] ); - } - $save_needed = true; + if ( ! $enabled ) { + $feature_class = $features[ $feature_key ] ?? null; + if ( ! $feature_class instanceof \Classifai\Features\Feature ) { + continue; } - } - // Save settings - if ( $save_needed ) { - update_option( $provider->get_option_name(), $settings ); + $settings = $feature_class->get_settings(); + // Disable the feature. + $settings['status'] = '0'; + update_option( $feature_class->get_option_name(), $settings ); } } @@ -221,9 +210,9 @@ public function handle_step_submission() { $step = 2; } - $onboarding_options['step_completed'] = $step; - $onboarding_options['enabled_features'] = $enabled_features; - $onboarding_options['configured_providers'] = array(); + $onboarding_options['step_completed'] = $step; + $onboarding_options['enabled_features'] = $enabled_features; + $onboarding_options['configured_features'] = array(); break; case 2: @@ -243,42 +232,38 @@ public function handle_step_submission() { case 3: // Bail if no provider provided. - if ( empty( $_POST['classifai-setup-provider'] ) ) { + if ( empty( $_POST['classifai-setup-feature'] ) ) { return; } - $providers = $this->get_providers(); - $provider_option = sanitize_text_field( wp_unslash( $_POST['classifai-setup-provider'] ) ); - $provider = $providers[ $provider_option ]; + $features = $this->get_features( false ); + $feature_key = sanitize_text_field( wp_unslash( $_POST['classifai-setup-feature'] ) ); + $feature = $features[ $feature_key ] ?? null; - if ( empty( $provider ) ) { + if ( ! $feature instanceof \Classifai\Features\Feature ) { return; } + $feature_option = $feature->get_option_name(); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $form_data = isset( $_POST[ $provider_option ] ) ? $this->classifai_sanitize( wp_unslash( $_POST[ $provider_option ] ) ) : array(); - - $settings = $provider->get_settings(); - $options = self::get_onboarding_options(); - $features = $options['enabled_features'] ?? array(); - $feature_data = $features[ $provider_option ] ?? array(); - - // Remove all features from the settings. - foreach ( $this->get_features() as $value ) { - $provider_features = $value['features'][ $provider_option ] ?? array(); - foreach ( $provider_features as $feature => $name ) { - if ( ! isset( $settings[ $feature ] ) ) { - continue; - } - unset( $settings[ $feature ] ); - } + $form_data = isset( $_POST[ $feature_option ] ) ? $this->classifai_sanitize( wp_unslash( $_POST[ $feature_option ] ) ) : array(); + + $settings = $feature->get_settings(); + $options = $this->get_onboarding_options(); + $enabled_features = $options['enabled_features'] ?? array(); + $is_enabled = isset( $enabled_features[ $feature_key ] ); + + if ( $is_enabled ) { + // Enable the feature. + $settings['status'] = '1'; } // Update the settings with the new values. - $settings = array_merge( $settings, $form_data, $feature_data ); + $settings = array_merge( $settings, $form_data ); // Save the ClassifAI settings. - update_option( $provider_option, $settings ); + update_option( $feature_option, $settings ); $setting_errors = get_settings_errors(); if ( ! empty( $setting_errors ) ) { @@ -286,21 +271,21 @@ public function handle_step_submission() { return; } - $onboarding_options = self::get_onboarding_options(); - $configured_providers = $onboarding_options['configured_providers'] ?? array(); + $onboarding_options = $this->get_onboarding_options(); + $configured_features = $onboarding_options['configured_features'] ?? array(); - $onboarding_options['configured_providers'] = array_unique( array_merge( $configured_providers, array( $provider_option ) ) ); + $onboarding_options['configured_features'] = array_unique( array_merge( $configured_features, array( $feature_key ) ) ); // Save the options to use it later steps. $this->update_onboarding_options( $onboarding_options ); // Redirect to next provider setup step. - $next_provider = $this->get_next_provider( $provider_option ); - if ( ! empty( $next_provider ) ) { + $next_feature = $this->get_next_feature( $feature_key ); + if ( ! empty( $next_feature ) ) { wp_safe_redirect( add_query_arg( array( 'step' => $step, - 'tab' => $next_provider, + 'tab' => $next_feature, ), $this->setup_url ) @@ -325,7 +310,9 @@ public function handle_step_submission() { } /** - * Sanitize variables using sanitize_text_field and wp_unslash. Arrays are cleaned recursively. + * Sanitize variables using sanitize_text_field and wp_unslash. + * + * Arrays are cleaned recursively. * Non-scalar values are ignored. * * @param string|array $data Data to sanitize. @@ -340,13 +327,12 @@ public function classifai_sanitize( $data ) { } /** - * Render classifai setup settings with the given fields. + * Render setup settings with the given fields. * * @param string $setting_name The name of the setting. * @param string[] $fields The fields to render. - * @return void */ - public static function render_classifai_setup_settings( $setting_name, $fields ) { + public function render_classifai_setup_settings( string $setting_name, array $fields = array() ) { global $wp_settings_sections, $wp_settings_fields; if ( ! isset( $wp_settings_fields[ $setting_name ][ $setting_name ] ) ) { @@ -366,7 +352,9 @@ public static function render_classifai_setup_settings( $setting_name, $fields ) if ( $section['title'] ) { ?> -

+

+ +

services; - if ( empty( $services ) || empty( $services['service_manager'] ) || ! $services['service_manager'] instanceof ServicesManager ) { - return []; + public function render_classifai_setup_feature( string $feature ) { + global $wp_settings_fields; + $features = $this->get_features( false ); + $feature_class = $features[ $feature ] ?? null; + if ( ! $feature_class instanceof \Classifai\Features\Feature ) { + return; } - /** @var ServicesManager $service_manager Instance of the services manager class. */ - $service_manager = $services['service_manager']; - $onboarding_features = []; + $setting_name = $feature_class->get_option_name(); + $section_name = $feature_class->get_option_name() . '_section'; + if ( ! isset( $wp_settings_fields[ $setting_name ][ $section_name ] ) ) { + return; + } - foreach ( $service_manager->service_classes as $service ) { - $display_name = $service->get_display_name(); - $service_slug = $service->get_menu_slug(); - $features = array(); + // Render the fields. + $skip = true; + $setting_fields = $wp_settings_fields[ $setting_name ][ $section_name ]; + foreach ( $setting_fields as $field_name => $field ) { + if ( 'provider' === $field_name ) { + $skip = false; + } - if ( empty( $service->provider_classes ) ) { + if ( $skip ) { continue; } - foreach ( $service->provider_classes as $provider_class ) { - $options = $provider_class->get_onboarding_options(); - if ( ! empty( $options ) && ! empty( $options['features'] ) ) { - $features[ $provider_class->get_option_name() ] = $options['features']; - } + if ( ! isset( $field['callback'] ) || ! is_callable( $field['callback'] ) ) { + continue; } - if ( ! empty( $features ) ) { - $onboarding_features[ $service_slug ] = array( - 'title' => $display_name, - 'features' => $features, - ); + $label_for = $field['args']['label_for'] ?? ''; + $class = $field['args']['class'] ?? ''; + + if ( 'ibm_watson_nlu_toggle' === $field_name ) { + ?> + + + + + + + + + + + + + + + + + services; - if ( empty( $services ) || empty( $services['service_manager'] ) || ! $services['service_manager'] instanceof ServicesManager ) { - return []; - } + public function get_features( bool $nested = true ): array { + if ( empty( $this->features ) ) { + $services = Plugin::$instance->services; + if ( empty( $services ) || empty( $services['service_manager'] ) || ! $services['service_manager'] instanceof ServicesManager ) { + return []; + } - /** @var ServicesManager $service_manager Instance of the services manager class. */ - $service_manager = $services['service_manager']; - $providers = []; + /** @var ServicesManager $service_manager Instance of the services manager class. */ + $service_manager = $services['service_manager']; + $onboarding_features = []; - foreach ( $service_manager->service_classes as $service ) { - if ( empty( $service->provider_classes ) ) { - continue; - } + foreach ( $service_manager->service_classes as $service ) { + $display_name = $service->get_display_name(); + $service_slug = $service->get_menu_slug(); + $features = array(); + if ( empty( $service->feature_classes ) ) { + continue; + } + + foreach ( $service->feature_classes as $feature_class ) { + if ( ! empty( $feature_class ) ) { + $features[ $feature_class::ID ] = $feature_class; + } + } - foreach ( $service->provider_classes as $provider_class ) { - $providers[ $provider_class->get_option_name() ] = $provider_class; + $onboarding_features[ $service_slug ] = array( + 'title' => $display_name, + 'features' => $features, + ); } + + $this->features = $onboarding_features; } - return $providers; - } + if ( $nested ) { + return $this->features; + } - /** - * Get Default features values. - * - * @return array The default feature values. - */ - public function get_default_features() { - $features = $this->get_features(); - $providers = $this->get_providers(); - $defaults = array(); - - foreach ( $features as $service_slug => $service ) { - foreach ( $service['features'] as $provider_slug => $provider ) { - $settings = $providers[ $provider_slug ]->get_settings(); - foreach ( $provider as $feature_slug => $feature ) { - $value = $settings[ $feature_slug ] ?? 'no'; - if ( count( explode( '__', $feature_slug ) ) > 1 ) { - $keys = explode( '__', $feature_slug ); - $value = $settings[ $keys[0] ][ $keys[1] ] ?? 'no'; - } elseif ( 'enable_image_captions' === $feature_slug ) { - $value = 'alt' === $settings['enable_image_captions']['alt'] ? 1 : 'no'; - } - $defaults[ $provider_slug ][ $feature_slug ] = $value; - } + if ( empty( $this->features ) ) { + return []; + } + + $features = []; + foreach ( $this->features as $service_slug => $service ) { + foreach ( $service['features'] as $feature_slug => $feature ) { + $features[ $feature_slug ] = $feature; } } - return $defaults; + return $features; } /** @@ -508,7 +524,7 @@ public function get_default_features() { * * @return array The onboarding options. */ - public static function get_onboarding_options() { + public function get_onboarding_options(): array { return get_option( 'classifai_onboarding_options', array() ); } @@ -517,8 +533,8 @@ public static function get_onboarding_options() { * * @return bool True if onboarding is completed, false otherwise. */ - public static function is_onboarding_completed() { - $options = self::get_onboarding_options(); + public function is_onboarding_completed(): bool { + $options = $this->get_onboarding_options(); return isset( $options['status'] ) && 'completed' === $options['status']; } @@ -527,12 +543,12 @@ public static function is_onboarding_completed() { * * @param array $options The options to update. */ - public function update_onboarding_options( $options ) { + public function update_onboarding_options( array $options ) { if ( ! is_array( $options ) ) { return; } - $onboarding_options = self::get_onboarding_options(); + $onboarding_options = $this->get_onboarding_options(); $onboarding_options = array_merge( $onboarding_options, $options ); // Update options. @@ -569,31 +585,32 @@ public function handle_skip_setup_step() { * * @return array Array of enabled providers. */ - public function get_enabled_providers() { - $providers = $this->get_providers(); - $onboarding_options = self::get_onboarding_options(); + public function get_enabled_features(): array { + $features = $this->get_features( false ); + $onboarding_options = $this->get_onboarding_options(); $enabled_features = $onboarding_options['enabled_features'] ?? array(); - $enabled_providers = []; - foreach ( array_keys( $enabled_features ) as $provider ) { - if ( ! empty( $providers[ $provider ] ) ) { - $enabled_providers[ $provider ] = $providers[ $provider ]->get_onboarding_options(); + foreach ( $enabled_features as $feature_key => $value ) { + if ( ! isset( $features[ $feature_key ] ) ) { + unset( $enabled_features[ $feature_key ] ); + continue; } + $enabled_features[ $feature_key ] = $features[ $feature_key ] ?? null; } - return $enabled_providers; + return $enabled_features; } /** - * Get next provider to setup. + * Get next feature to setup. * - * @param string $current_provider Current provider. + * @param string $current_feature Current feature. * @return string|bool Next provider to setup or false if none. */ - public function get_next_provider( $current_provider ) { - $enabled_providers = $this->get_enabled_providers(); - $keys = array_keys( $enabled_providers ); - $index = array_search( $current_provider, $keys, true ); + public function get_next_feature( string $current_feature ) { + $enabled_features = $this->get_enabled_features(); + $keys = array_keys( $enabled_features ); + $index = array_search( $current_feature, $keys, true ); if ( false === $index ) { return false; @@ -617,7 +634,7 @@ public function prevent_direct_step_visits() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $step = absint( wp_unslash( $_GET['step'] ) ); - $onboarding_options = self::get_onboarding_options(); + $onboarding_options = $this->get_onboarding_options(); $step_completed = isset( $onboarding_options['step_completed'] ) ? absint( $onboarding_options['step_completed'] ) : 0; if ( ( $step_completed + 1 ) < $step ) { @@ -625,39 +642,22 @@ public function prevent_direct_step_visits() { } } - /** - * Check if any of the providers are configured. - * - * @return boolean - */ - public function has_configured_providers() { - $providers = $this->get_providers(); - - foreach ( $providers as $provider ) { - if ( $provider->is_configured() ) { - return true; - } - } - - return false; - } - /** * Get configured features. * * @return array */ - public function get_configured_features() { - $features = $this->get_features(); + public function get_configured_features(): array { + $features = $this->get_features( false ); $configured_features = array(); - foreach ( $features as $feature ) { - foreach ( $feature['features'] as $provider_key => $provider_features ) { - foreach ( $provider_features as $feature_key => $feature_options ) { - if ( $feature_options['enabled'] ) { - $configured_features[ $provider_key ][] = $feature_key; - } - } + foreach ( $features as $feature_key => $feature_class ) { + if ( ! $feature_class instanceof \Classifai\Features\Feature ) { + continue; + } + $settings = $feature_class->get_settings(); + if ( '1' === $settings['status'] ) { + $configured_features[] = $feature_key; } } diff --git a/includes/Classifai/Admin/Update.php b/includes/Classifai/Admin/Update.php index 04c8e224a..212060f98 100644 --- a/includes/Classifai/Admin/Update.php +++ b/includes/Classifai/Admin/Update.php @@ -2,7 +2,7 @@ /** * ClassifAI Auto Update Integration * - * @package 10up/classifai + * @package classifai */ namespace Classifai\Admin; @@ -33,7 +33,7 @@ class Update { * * @return bool */ - public function can_register() { + public function can_register(): bool { return class_exists( '\YahnisElsts\PluginUpdateChecker\v5\PucFactory' ) && self::license_check(); } diff --git a/includes/Classifai/Admin/UserProfile.php b/includes/Classifai/Admin/UserProfile.php index cd33df529..4982f2862 100644 --- a/includes/Classifai/Admin/UserProfile.php +++ b/includes/Classifai/Admin/UserProfile.php @@ -2,8 +2,7 @@ namespace Classifai\Admin; -use Classifai\Providers\AccessControl; -use Classifai\Providers\Provider; +use Classifai\Features\Feature; use Classifai\Services\Service; use function Classifai\get_plugin; @@ -34,10 +33,9 @@ public function init() { } /** - * Add ClassifAI features opt-out checkboxes to user profile and edit user. + * Add features opt-out checkboxes to user profile and edit user. * * @param \WP_User $user User object. - * @return void */ public function user_settings( \WP_User $user ) { $user_id = $user->ID; @@ -47,7 +45,7 @@ public function user_settings( \WP_User $user ) { return; } - // Bail if user is not allowed to access ClassifAI features. + // Bail if user is not allowed to access features. $features = $this->get_allowed_features( $user->ID ); if ( empty( $features ) ) { return; @@ -89,12 +87,11 @@ public function user_settings( \WP_User $user ) { } /** - * Save ClassifAI features opt-out settings. + * Save features opt-out settings. * * @param int $user_id User ID. - * @return void */ - public function save_user_settings( $user_id ) { + public function save_user_settings( int $user_id ) { if ( ! isset( $_POST['classifai_out_out_features_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['classifai_out_out_features_nonce'] ) ), 'classifai_out_out_features' ) @@ -119,7 +116,7 @@ public function save_user_settings( $user_id ) { * @param int $user_id User ID. * @return array List of features. */ - public function get_allowed_features( $user_id ) { + public function get_allowed_features( int $user_id ): array { $user = get_user_by( 'id', $user_id ); if ( ! $user ) { return array(); @@ -135,49 +132,49 @@ public function get_allowed_features( $user_id ) { $service_classes = $services['service_manager']->service_classes; foreach ( $service_classes as $service_class ) { - if ( ! $service_class instanceof Service || empty( $service_class->provider_classes ) ) { + if ( ! $service_class instanceof Service || empty( $service_class->feature_classes ) ) { continue; } - foreach ( $service_class->provider_classes as $provider_class ) { - if ( ! $provider_class instanceof Provider ) { + foreach ( $service_class->feature_classes as $feature_class ) { + if ( ! $feature_class instanceof Feature || ! $feature_class->is_enabled() ) { continue; } - $provider_features = $provider_class->get_features(); - if ( empty( $provider_features ) ) { + + $settings = $feature_class->get_settings(); + // Bail if feature settings are empty. + if ( empty( $settings ) ) { + continue; + } + + $role_based_access_enabled = isset( $settings['role_based_access'] ) && 1 === (int) $settings['role_based_access']; + $user_based_access_enabled = isset( $settings['user_based_access'] ) && 1 === (int) $settings['user_based_access']; + $user_based_opt_out_enabled = isset( $settings['user_based_opt_out'] ) && 1 === (int) $settings['user_based_opt_out']; + + // Bail if user opt-out is not enabled. + if ( ! $user_based_opt_out_enabled ) { continue; } - foreach ( $provider_features as $feature => $feature_name ) { - // Check if feature is enabled. - if ( ! $provider_class->is_enabled( $feature ) ) { - continue; - } - - $access_control = new AccessControl( $provider_class, $feature ); - - // Check if feature has user based opt-out enabled. - if ( $access_control->is_user_based_opt_out_enabled() ) { - // Check if user has access to the feature by role. - $allowed_roles = $access_control->get_allowed_roles(); - if ( - $access_control->is_role_based_access_enabled() && - ! empty( $allowed_roles ) && - ! empty( array_intersect( $user_roles, $allowed_roles ) ) - ) { - $allowed_features[ $feature ] = $feature_name; - continue; - } - - // Check if user has access to the feature. - $allowed_users = $access_control->get_allowed_users(); - if ( - $access_control->is_user_based_access_enabled() && - ! empty( $allowed_users ) && - in_array( $user_id, $allowed_users, true ) - ) { - $allowed_features[ $feature ] = $feature_name; - } - } + + // Check if user has access to the feature by role. + $allowed_roles = $settings['roles'] ?? []; + if ( + $role_based_access_enabled && + ! empty( $allowed_roles ) && + ! empty( array_intersect( $user_roles, $allowed_roles ) ) + ) { + $allowed_features[ $feature_class::ID ] = $feature_class->get_label(); + continue; + } + + // Check if user has access to the feature. + $allowed_users = $settings['users'] ?? []; + if ( + $user_based_access_enabled && + ! empty( $allowed_users ) && + in_array( $user_id, $allowed_users, true ) + ) { + $allowed_features[ $feature_class::ID ] = $feature_class->get_label(); } } } diff --git a/includes/Classifai/Admin/templates/onboarding-step-four.php b/includes/Classifai/Admin/templates/onboarding-step-four.php index f394f06b5..00e44f0bf 100644 --- a/includes/Classifai/Admin/templates/onboarding-step-four.php +++ b/includes/Classifai/Admin/templates/onboarding-step-four.php @@ -5,11 +5,11 @@ * @package ClassifAI */ -$onboarding_options = get_option( 'classifai_onboarding_options', array() ); -$enabled_features = $onboarding_options['enabled_features'] ?? array(); -$configured_providers = $onboarding_options['configured_providers'] ?? array(); -$onboarding = new Classifai\Admin\Onboarding(); -$features = $onboarding->get_features(); +$onboarding_options = get_option( 'classifai_onboarding_options', array() ); +$enabled_features = $onboarding_options['enabled_features'] ?? array(); +$configured_features = $onboarding_options['configured_features'] ?? array(); +$onboarding = new Classifai\Admin\Onboarding(); +$features = $onboarding->get_features(); $args = array( 'step' => 4, @@ -46,28 +46,22 @@
    $provider_features ) { - foreach ( $provider_features as $feature_key => $feature_options ) { - $enabled = isset( $enabled_features[ $provider ][ $feature_key ] ); - if ( count( explode( '__', $feature_key ) ) > 1 ) { - $keys = explode( '__', $feature_key ); - $enabled = isset( $enabled_features[ $provider ][ $keys[0] ][ $keys[1] ] ); - } - - if ( ! $enabled ) { - continue; - } - - $icon_class = ( $feature_options['enabled'] ) ? 'dashicons-yes-alt' : 'dashicons-dismiss'; - ?> -
  • - - -
  • - $feature_class ) { + $enabled = isset( $enabled_features[ $feature_key ] ); + if ( ! $enabled ) { + continue; } + + $is_configured = $feature_class->is_feature_enabled(); + $icon_class = $is_configured ? 'dashicons-yes-alt' : 'dashicons-dismiss'; + ?> +
  • + + +
  • +
diff --git a/includes/Classifai/Admin/templates/onboarding-step-one.php b/includes/Classifai/Admin/templates/onboarding-step-one.php index 10a53573e..c4c940bc7 100644 --- a/includes/Classifai/Admin/templates/onboarding-step-one.php +++ b/includes/Classifai/Admin/templates/onboarding-step-one.php @@ -7,8 +7,7 @@ $onboarding = new Classifai\Admin\Onboarding(); $features = $onboarding->get_features(); -$has_configured = $onboarding->has_configured_providers(); -$onboarding_options = Classifai\Admin\Onboarding::get_onboarding_options(); +$onboarding_options = $onboarding->get_onboarding_options(); $enabled_features = $onboarding_options['enabled_features'] ?? array(); $args = array( @@ -45,40 +44,24 @@
    $provider_features ) { - foreach ( $provider_features as $feature_key => $feature_options ) { - $checked = false; - if ( $has_configured ) { - // For existing users, enable features based on their saved configuration. - $checked = $feature_options['enabled'] ?? false; - } elseif ( ! empty( $enabled_features ) ) { - // Enable features based on user selection. - $checked = isset( $enabled_features[ $provider ][ $feature_key ] ); - if ( count( explode( '__', $feature_key ) ) > 1 ) { - $keys = explode( '__', $feature_key ); - $checked = isset( $enabled_features[ $provider ][ $keys[0] ][ $keys[1] ] ); - } - } else { - // Enable all features by default. - $checked = true; - if ( strpos( $feature_key, 'post_types__' ) !== false ) { - if ( ! in_array( str_replace( 'post_types__', '', $feature_key ), array( 'post', 'page' ), true ) ) { - $checked = false; - } - } - } - ?> -
  • - -
  • - $feature_class ) { + if ( ! $feature_class instanceof Classifai\Features\Feature ) { + continue; } + $feature_label = $feature_class->get_label(); + $settings = $feature_class->get_settings(); + $checked = isset( $enabled_features[ $feature_key ] ) ? $enabled_features[ $feature_key ] : $settings['status']; + ?> +
  • + +
  • +
diff --git a/includes/Classifai/Admin/templates/onboarding-step-three.php b/includes/Classifai/Admin/templates/onboarding-step-three.php index 72d7a322c..c96a10787 100644 --- a/includes/Classifai/Admin/templates/onboarding-step-three.php +++ b/includes/Classifai/Admin/templates/onboarding-step-three.php @@ -5,13 +5,15 @@ * @package ClassifAI */ -$base_url = admin_url( 'admin.php?page=classifai_setup&step=3' ); -$onboarding = new Classifai\Admin\Onboarding(); -$enabled_providers = $onboarding->get_enabled_providers(); -$current_provider = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : array_key_first( $enabled_providers ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -$next_provider = $onboarding->get_next_provider( $current_provider ); -$skip_url = add_query_arg( 'tab', $next_provider, $base_url ); -if ( empty( $next_provider ) ) { +$base_url = admin_url( 'admin.php?page=classifai_setup&step=3' ); +$onboarding = new Classifai\Admin\Onboarding(); +$enabled_features = $onboarding->get_enabled_features(); +$onboarding_options = $onboarding->get_onboarding_options(); +$configured_features = $onboarding_options['configured_features'] ?? array(); +$current_feature = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : array_key_first( $enabled_features ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended +$next_feature = $onboarding->get_next_feature( $current_feature ); +$skip_url = add_query_arg( 'tab', $next_feature, $base_url ); +if ( empty( $next_feature ) ) { $skip_url = wp_nonce_url( admin_url( 'admin-post.php?action=classifai_skip_step&step=3' ), 'classifai_skip_step_action', 'classifai_skip_step_nonce' ); } @@ -31,34 +33,46 @@ // Header require_once 'onboarding-header.php'; ?> - -
- $provider ) { - $provider_url = add_query_arg( 'tab', $key, $base_url ); - $is_active = ( $current_provider === $key ) ? 'active' : ''; - ?> - - - +
+
-
-
- - $feature_class ) { + $is_configured = in_array( $key, $configured_features, true ) ? true : false; + $feature_url = add_query_arg( 'tab', $key, $base_url ); + $is_active = ( $current_feature === $key ) ? 'active' : ''; + $icon_class = 'dashicons-clock'; + if ( $is_configured ) { + $icon_class = 'dashicons-yes-alt'; + } elseif ( array_search( $current_feature, $feature_keys, true ) > array_search( $key, $feature_keys, true ) ) { + $icon_class = 'dashicons-warning'; + } + ?> + + + get_label() ); ?> + + -

- -

- +
+
+ + + render_classifai_setup_feature( $current_feature ); + } else { + ?> +

+ +

+ +
+
2, 'title' => __( 'Register ClassifAI', 'classifai' ), 'left_link' => array( @@ -24,7 +25,7 @@
render_classifai_setup_settings( 'classifai_settings', array( 'email', 'registration-key' ) ); ?>
diff --git a/includes/Classifai/Blocks/recommended-content-block/register.php b/includes/Classifai/Blocks/recommended-content-block/register.php index 0522cac03..c905b2995 100644 --- a/includes/Classifai/Blocks/recommended-content-block/register.php +++ b/includes/Classifai/Blocks/recommended-content-block/register.php @@ -49,10 +49,9 @@ function register() { * Render callback method for the block * * @param array $attributes The blocks attributes. - * * @return string The rendered block markup. */ -function render_block_callback( $attributes ) { +function render_block_callback( array $attributes ): string { // Render block in Gutenberg Editor. if ( defined( 'REST_REQUEST' ) && \REST_REQUEST ) { $personalizer = new Personalizer( false ); diff --git a/includes/Classifai/Command/ClassifaiCommand.php b/includes/Classifai/Command/ClassifaiCommand.php index 92193535a..487bbf302 100644 --- a/includes/Classifai/Command/ClassifaiCommand.php +++ b/includes/Classifai/Command/ClassifaiCommand.php @@ -2,19 +2,22 @@ namespace Classifai\Command; -use Classifai\Admin\SavePostHandler; -use Classifai\Watson\APIRequest; -use Classifai\Watson\Classifier; -use Classifai\Watson\Normalizer; -use Classifai\PostClassifier; +use Classifai\Features\AudioTranscriptsGeneration; +use Classifai\Features\Classification; +use Classifai\Features\ExcerptGeneration; +use Classifai\Features\ImageCropping; +use Classifai\Features\TextToSpeech; +use Classifai\Providers\Watson\APIRequest; +use Classifai\Providers\Watson\Classifier; +use Classifai\Normalizer; +use Classifai\Providers\Watson\PostClassifier; use Classifai\Providers\Azure\ComputerVision; use Classifai\Providers\Azure\SmartCropping; -use Classifai\Providers\Azure\TextToSpeech; -use Classifai\Providers\OpenAI\Whisper; -use Classifai\Providers\OpenAI\Whisper\Transcribe; -use Classifai\Providers\OpenAI\ChatGPT; use Classifai\Providers\OpenAI\Embeddings; +use function Classifai\Providers\Watson\get_username; +use function Classifai\Providers\Watson\get_password; + /** * ClassifaiCommand is the command line interface of the ClassifAI plugin. * It provides subcommands to test classification results and batch @@ -151,8 +154,8 @@ public function text( $args = [], $opts = [] ) { $opts = wp_parse_args( $opts, $defaults ); $classifier = new Classifier(); - $username = \Classifai\get_watson_username(); - $password = \Classifai\get_watson_password(); + $username = get_username(); + $password = get_password(); if ( empty( $username ) ) { \WP_CLI::error( 'Watson Username not found in options or constant.' ); @@ -241,15 +244,14 @@ public function text_to_speech( $args = [], $opts = [] ) { 'per_page' => 100, ]; + $feature_speech = new TextToSpeech(); + $allowed_post_types = $feature_speech->get_supported_post_types(); $opts = wp_parse_args( $opts, $defaults ); $opts['per_page'] = (int) $opts['per_page'] > 0 ? $opts['per_page'] : 100; - $allowed_post_types = TextToSpeech::get_supported_post_types(); $count = 0; $errors = 0; - $save_post_handler = new SavePostHandler(); - // Determine if this is a dry run or not. if ( isset( $opts['dry-run'] ) ) { if ( 'false' === $opts['dry-run'] ) { @@ -296,7 +298,10 @@ public function text_to_speech( $args = [], $opts = [] ) { foreach ( $posts as $post_id ) { if ( ! $dry_run ) { - $result = $save_post_handler->synthesize_speech( $post_id ); + $result = $feature_speech->run( $post_id, 'synthesize' ); + if ( $result && ! is_wp_error( $result ) ) { + $result = $feature_speech->save( $result, $post_id ); + } if ( is_wp_error( $result ) ) { \WP_CLI::log( sprintf( 'Error while processing item ID %s: %s', $post_id, $result->get_error_message() ) ); @@ -344,7 +349,10 @@ public function text_to_speech( $args = [], $opts = [] ) { } if ( ! $dry_run ) { - $result = $save_post_handler->synthesize_speech( $post_id ); + $result = $feature_speech->run( $post_id, 'synthesize' ); + if ( $result && ! is_wp_error( $result ) ) { + $result = $feature_speech->save( $result, $post_id ); + } if ( is_wp_error( $result ) ) { \WP_CLI::log( sprintf( 'Error while processing item ID %s: %s', $post_id, $result->get_error_message() ) ); @@ -400,8 +408,9 @@ public function transcribe_audio( $args = [], $opts = [] ) { $count = 0; $errors = 0; - $whisper = new Whisper( false ); - $settings = $whisper->get_settings(); + $audio_transcription = new AudioTranscriptsGeneration(); + $feature_settings = $audio_transcription->get_settings(); + $provider_instance = $audio_transcription->get_feature_provider_instance( $feature_settings['provider'] ); // Determine if this is a dry run or not. if ( isset( $opts['dry-run'] ) ) { @@ -428,19 +437,20 @@ public function transcribe_audio( $args = [], $opts = [] ) { foreach ( $attachment_ids as $attachment_id ) { $attachment = get_post( $attachment_id ); - $transcribe = new Transcribe( $attachment_id, $settings ); - if ( ! $this->should_transcribe_attachment( $attachment, $attachment_id, $transcribe, (bool) $opts['force'] ) ) { + if ( ! $this->should_transcribe_attachment( $attachment, $attachment_id, $audio_transcription, (bool) $opts['force'] ) ) { ++$errors; continue; } if ( ! $dry_run ) { - $result = $transcribe->process(); + $result = $audio_transcription->run( $attachment_id, 'transcript' ); if ( is_wp_error( $result ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %s: %s', $attachment_id, $result->get_error_message() ), false ); ++$errors; + } else { + $result = $audio_transcription->add_transcription( $result, $attachment_id ); } } @@ -454,12 +464,11 @@ public function transcribe_audio( $args = [], $opts = [] ) { $paged = 1; $mime_types = []; - $transcribe = new Transcribe( 1, [] ); // Get all the mime types for the file formats we support. foreach ( wp_get_mime_types() as $extensions => $mime ) { foreach ( explode( '|', $extensions ) as $ext ) { - if ( in_array( $ext, $transcribe->file_formats, true ) ) { + if ( in_array( $ext, $provider_instance->file_formats ?? [ 'mp3' ], true ) ) { $mime_types[] = $mime; } } @@ -480,19 +489,20 @@ public function transcribe_audio( $args = [], $opts = [] ) { foreach ( $attachments as $attachment_id ) { $attachment = get_post( $attachment_id ); - $transcribe = new Transcribe( $attachment_id, $settings ); - if ( ! $this->should_transcribe_attachment( $attachment, (int) $attachment_id, $transcribe, (bool) $opts['force'] ) ) { + if ( ! $this->should_transcribe_attachment( $attachment, (int) $attachment_id, $audio_transcription, (bool) $opts['force'] ) ) { ++$errors; continue; } if ( ! $dry_run ) { - $result = $transcribe->process(); + $result = $audio_transcription->run( $attachment_id, 'transcript' ); if ( is_wp_error( $result ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %s: %s', $attachment_id, $result->get_error_message() ), false ); ++$errors; + } else { + $result = $audio_transcription->add_transcription( $result, $attachment_id ); } } @@ -563,8 +573,6 @@ public function generate_excerpt( $args = [], $opts = [] ) { $errors = 0; $skipped = 0; - $chat_gpt = new ChatGPT( false ); - // Determine if this is a dry run or not. if ( isset( $opts['dry-run'] ) ) { if ( 'false' === $opts['dry-run'] ) { @@ -616,7 +624,7 @@ public function generate_excerpt( $args = [], $opts = [] ) { continue; } - $result = $chat_gpt->generate_excerpt( (int) $post->ID ); + $result = ( new ExcerptGeneration() )->run( $post->ID, 'excerpt' ); if ( is_wp_error( $result ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %d: %s', $post->ID, $result->get_error_message() ), false ); @@ -669,7 +677,7 @@ public function generate_excerpt( $args = [], $opts = [] ) { continue; } - $result = $chat_gpt->generate_excerpt( (int) $post_id ); + $result = ( new ExcerptGeneration() )->run( $post_id, 'excerpt' ); if ( is_wp_error( $result ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %d: %s', $post_id, $result->get_error_message() ), false ); @@ -709,13 +717,13 @@ public function generate_excerpt( $args = [], $opts = [] ) { /** * Determine if an attachment should be transcribed. * - * @param \WP_Post|null $attachment Attachment we are processing. - * @param int $attachment_id Attachment ID. - * @param Transcribe $transcribe Transcribe instance. - * @param boolean $force Whether to force processing. - * @return boolean + * @param \WP_Post|null $attachment Attachment we are processing. + * @param int $attachment_id Attachment ID. + * @param AudioTranscriptsGeneration $audio_transcription AudioTranscriptsGeneration instance. + * @param bool $force Whether to force processing. + * @return bool */ - private function should_transcribe_attachment( $attachment, int $attachment_id, Transcribe $transcribe, bool $force = false ) { + private function should_transcribe_attachment( $attachment, int $attachment_id, AudioTranscriptsGeneration $audio_transcription, bool $force = false ) { // Ensure we have a valid ID. if ( ! $attachment ) { \WP_CLI::error( sprintf( 'Item ID %d does not exist', $attachment_id ), false ); @@ -729,8 +737,11 @@ private function should_transcribe_attachment( $attachment, int $attachment_id, } // Ensure the attachment meets the requirements for processing. - if ( ! $transcribe->should_process( $attachment_id ) ) { - \WP_CLI::error( sprintf( 'Item ID %d does not meet processing requirements. Ensure the file type is one of %s and file size is under %d bytes.', $attachment_id, implode( ', ', $transcribe->file_formats ), $transcribe->max_file_size ), false ); + if ( ! $audio_transcription->should_process( $attachment_id ) ) { + $feature_settings = $audio_transcription->get_settings(); + $provider_instance = $audio_transcription->get_feature_provider_instance( $feature_settings['provider'] ); + + \WP_CLI::error( sprintf( 'Item ID %d does not meet processing requirements. Ensure the file type is one of %s and file size is under %d bytes.', $attachment_id, implode( ', ', $provider_instance->file_formats ?? [ 'mp3' ] ), $provider_instance->max_file_size ?? 25 * MB_IN_BYTES ), false ); return false; } @@ -835,9 +846,10 @@ public function image( $args = [], $opts = [] ) { * @param array $opts Options. */ public function crop( $args = [], $opts = [] ) { - $classifier = new ComputerVision( false ); - $settings = $classifier->get_settings(); - $smart_cropping = new SmartCropping( $settings ); + $image_cropping = new ImageCropping(); + $provider = $image_cropping->get_feature_provider_instance(); + $provider_class = get_class( $provider ); + $settings = $image_cropping->get_settings( $provider_class::ID ); $default_opts = [ 'limit' => false, ]; @@ -874,19 +886,24 @@ public function crop( $args = [], $opts = [] ) { $current_meta = wp_get_attachment_metadata( $attachment_id ); foreach ( $current_meta['sizes'] as $size => $size_data ) { - if ( ! $smart_cropping->should_crop( $size ) ) { - continue; - } + switch ( $provider_class::ID ) { + case ComputerVision::ID: + $smart_cropping = new SmartCropping( $settings ); - $data = [ - 'width' => $size_data['width'], - 'height' => $size_data['height'], - ]; + if ( ! $smart_cropping->should_crop( $size ) ) { + break; + } - $smart_thumbnail = $smart_cropping->get_cropped_thumbnail( $attachment_id, $data ); + $data = [ + 'width' => $size_data['width'], + 'height' => $size_data['height'], + ]; - if ( is_wp_error( $smart_thumbnail ) ) { - $errors[ $attachment_id . ':' . $size_data['width'] . 'x' . $size_data['height'] ] = $smart_thumbnail; + $smart_thumbnail = $smart_cropping->get_cropped_thumbnail( $attachment_id, $data ); + if ( is_wp_error( $smart_thumbnail ) ) { + $errors[ $attachment_id . ':' . $size_data['width'] . 'x' . $size_data['height'] ] = $smart_thumbnail; + } + break; } } } @@ -944,6 +961,13 @@ public function embeddings( $args = [], $opts = [] ) { 'per_page' => 100, ]; + $feature = new Classification(); + $provider = $feature->get_feature_provider_instance(); + + if ( Embeddings::ID !== $provider::ID ) { + \WP_CLI::error( 'This command is only available for the OpenAI Embeddings feature' ); + } + $embeddings = new Embeddings( false ); $opts = wp_parse_args( $opts, $defaults ); $opts['per_page'] = (int) $opts['per_page'] > 0 ? $opts['per_page'] : 100; @@ -999,7 +1023,7 @@ public function embeddings( $args = [], $opts = [] ) { foreach ( $posts as $post_id ) { if ( ! $dry_run ) { - $result = $embeddings->generate_embeddings_for_post( $post_id ); + $result = $feature->run( $post_id ); if ( is_wp_error( $result ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %s', $post_id ), false ); @@ -1047,7 +1071,7 @@ public function embeddings( $args = [], $opts = [] ) { } if ( ! $dry_run ) { - $result = $embeddings->generate_embeddings_for_post( $post_id ); + $result = $feature->run( $post_id ); if ( is_wp_error( $result ) ) { \WP_CLI::error( sprintf( 'Error while processing item ID %s', $post_id ), false ); @@ -1079,8 +1103,8 @@ public function embeddings( $args = [], $opts = [] ) { * @param array $opts Options. */ public function auth( $args = [], $opts = [] ) { - $username = \Classifai\get_watson_username(); - $password = \Classifai\get_watson_password(); + $username = get_username(); + $password = get_password(); if ( empty( $username ) ) { \WP_CLI::error( 'Watson Username not found in options or constant.' ); diff --git a/includes/Classifai/Features/AudioTranscriptsGeneration.php b/includes/Classifai/Features/AudioTranscriptsGeneration.php new file mode 100644 index 000000000..a37e723e0 --- /dev/null +++ b/includes/Classifai/Features/AudioTranscriptsGeneration.php @@ -0,0 +1,350 @@ +label = __( 'Audio Transcripts Generation', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + Whisper::ID => __( 'OpenAI Whisper', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_transcribe_audio' ] ); + add_action( 'add_attachment', [ $this, 'transcribe_audio' ] ); + + add_filter( 'attachment_fields_to_edit', [ $this, 'add_buttons_to_media_modal' ], 10, 2 ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'generate-transcript/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Attachment ID to generate transcript for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'generate_audio_transcript_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate a transcript. + * + * This check ensures we have a valid user with proper capabilities + * making the request, that we are properly authenticated with OpenAI + * and that transcription is turned on. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function generate_audio_transcript_permissions_check( WP_REST_Request $request ) { + $attachment_id = $request->get_param( 'id' ); + $post_type = get_post_type_object( 'attachment' ); + + // Ensure attachments are allowed in REST endpoints. + if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) { + return false; + } + + // Ensure we have a logged in user that can upload and change files. + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( 'upload_files' ) ) { + return false; + } + + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Audio transciption is not currently enabled.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/generate-transcript' ) === 0 ) { + $result = $this->run( $request->get_param( 'id' ), 'transcript' ); + + if ( ! is_wp_error( $result ) ) { + $result = $this->add_transcription( $result, $request->get_param( 'id' ) ); + } + + return rest_ensure_response( $result ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Enqueue assets. + */ + public function enqueue_admin_assets() { + wp_enqueue_script( + 'classifai-media-script', + CLASSIFAI_PLUGIN_URL . 'dist/media.js', + array_merge( get_asset_info( 'media', 'dependencies' ), array( 'jquery', 'media-editor', 'lodash' ) ), + get_asset_info( 'media', 'version' ), + true + ); + } + + /** + * Add new buttons to the media modal. + * + * @param array $form_fields Existing form fields. + * @param \WP_Post $attachment Attachment object. + * @return array + */ + public function add_buttons_to_media_modal( array $form_fields, \WP_Post $attachment ): array { + if ( ! $this->should_process( $attachment->ID ) ) { + return $form_fields; + } + + $text = empty( get_the_content( null, false, $attachment ) ) ? __( 'Transcribe', 'classifai' ) : __( 'Re-transcribe', 'classifai' ); + + $form_fields['retranscribe'] = [ + 'label' => __( 'Transcribe audio', 'classifai' ), + 'input' => 'html', + 'html' => '', + 'show_in_edit' => false, + ]; + + return $form_fields; + } + + /** + * Add metabox on single attachment view to allow for transcription. + * + * @param \WP_Post $post Post object. + */ + public function setup_attachment_meta_box( \WP_Post $post ) { + if ( ! $this->should_process( $post->ID ) ) { + return; + } + + add_meta_box( + 'attachment_meta_box', + __( 'ClassifAI Audio Processing', 'classifai' ), + [ $this, 'attachment_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Display the attachment meta box. + * + * @param \WP_Post $post Post object. + */ + public function attachment_meta_box( \WP_Post $post ) { + $text = empty( get_the_content( null, false, $post ) ) ? __( 'Transcribe', 'classifai' ) : __( 'Re-transcribe', 'classifai' ); + + wp_nonce_field( 'classifai_audio_transcript_meta_action', 'classifai_audio_transcript_meta' ); + ?> + +
+
+ +
+
+ + run( $attachment_id, 'transcript' ); + + if ( ! is_wp_error( $result ) ) { + $result = $this->add_transcription( $result, $attachment_id ); + } + + return $result; + } + + /** + * Transcribe audio on attachment save, if option is selected. + * + * @param int $attachment_id Attachment ID. + * @return WP_Error|string|null + */ + public function maybe_transcribe_audio( int $attachment_id ) { + if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $attachment_id ) ) { + return; + } + + if ( empty( $_POST['classifai_audio_transcript_meta'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['classifai_audio_transcript_meta'] ) ), 'classifai_audio_transcript_meta_action' ) ) { + return; + } + + if ( clean_input( 'retranscribe' ) ) { + // Remove to avoid infinite loop. + remove_action( 'edit_attachment', [ $this, 'maybe_transcribe_audio' ] ); + + return $this->transcribe_audio( $attachment_id ); + } + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'Enabling this will automatically generate transcripts for supported audio files.', 'classifai' ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'provider' => Whisper::ID, + ]; + } + + /** + * Should this attachment be processed. + * + * Ensure the file is a supported format and is under the maximum file size. + * + * @param int $attachment_id Attachment ID to process. + * @return bool + */ + public function should_process( int $attachment_id ): bool { + $settings = $this->get_settings(); + $provider_id = $settings['provider']; + $provider_instance = $this->get_feature_provider_instance( $provider_id ); + + $mime_type = get_post_mime_type( $attachment_id ); + $matched_extensions = explode( '|', array_search( $mime_type, wp_get_mime_types(), true ) ); + $process = false; + + foreach ( $matched_extensions as $ext ) { + if ( in_array( $ext, $provider_instance->file_formats ?? [ 'mp3' ], true ) ) { + $process = true; + } + } + + // If we have a proper file format, check the file size. + if ( $process ) { + $filesize = filesize( get_attached_file( $attachment_id ) ); + if ( ! $filesize || $filesize > $provider_instance->max_file_size ?? 25 * MB_IN_BYTES ) { + $process = false; + } + } + + return $process; + } + + /** + * Add the transcribed text to the attachment. + * + * @param string $text Transcription result. + * @param int $attachment_id Attachment ID. + * @return string|WP_Error + */ + public function add_transcription( string $text = '', int $attachment_id = 0 ) { + if ( empty( $text ) ) { + return new WP_Error( 'invalid_result', esc_html__( 'The transcription result is invalid.', 'classifai' ) ); + } + + /** + * Filter the text result returned from Whisper API. + * + * @since 2.2.0 + * @hook classifai_whisper_transcribe_result + * + * @param {string} $text Text extracted from the response. + * @param {int} $attachment_id The attachment ID. + * + * @return {string} + */ + $text = apply_filters( 'classifai_whisper_transcribe_result', $text, $attachment_id ); + + $update = wp_update_post( + [ + 'ID' => (int) $attachment_id, + 'post_content' => wp_kses_post( $text ), + ], + true + ); + + if ( is_wp_error( $update ) ) { + return $update; + } else { + return $text; + } + } +} diff --git a/includes/Classifai/Features/Classification.php b/includes/Classifai/Features/Classification.php new file mode 100644 index 000000000..f26d161e8 --- /dev/null +++ b/includes/Classifai/Features/Classification.php @@ -0,0 +1,157 @@ +label = __( 'Classification', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + NLU::ID => __( 'IBM Watson NLU', 'classifai' ), + Embeddings::ID => __( 'OpenAI Embeddings', 'classifai' ), + ]; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'Enables the automatic content classification of posts.', 'classifai' ); + } + + /** + * Add any needed custom fields. + */ + public function add_custom_settings_fields() { + $settings = $this->get_settings(); + $post_statuses = get_post_statuses_for_language_settings(); + + add_settings_field( + 'post_statuses', + esc_html__( 'Post statuses', 'classifai' ), + [ $this, 'render_checkbox_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'post_statuses', + 'options' => $post_statuses, + 'default_values' => $settings['post_statuses'], + 'description' => __( 'Choose which post statuses are allowed to use this feature.', 'classifai' ), + ] + ); + + $post_types = get_post_types_for_language_settings(); + $post_type_options = array(); + + foreach ( $post_types as $post_type ) { + $post_type_options[ $post_type->name ] = $post_type->label; + } + + add_settings_field( + 'post_types', + esc_html__( 'Post types', 'classifai' ), + [ $this, 'render_checkbox_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'post_types', + 'options' => $post_type_options, + 'default_values' => $settings['post_types'], + 'description' => __( 'Choose which post types are allowed to use this feature.', 'classifai' ), + ] + ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'post_statuses' => [ + 'publish' => 1, + ], + 'post_types' => [ + 'post' => 1, + ], + 'provider' => NLU::ID, + ]; + } + + /** + * Sanitizes the default feature settings. + * + * @param array $new_settings Settings being saved. + * @return array + */ + public function sanitize_default_feature_settings( array $new_settings ): array { + $settings = $this->get_settings(); + + $new_settings['post_statuses'] = isset( $new_settings['post_statuses'] ) ? array_map( 'sanitize_text_field', $new_settings['post_statuses'] ) : $settings['post_statuses']; + $new_settings['post_types'] = isset( $new_settings['post_types'] ) ? array_map( 'sanitize_text_field', $new_settings['post_types'] ) : $settings['post_types']; + + return $new_settings; + } + + /** + * Runs the feature. + * + * @param mixed ...$args Arguments required by the feature depending on the provider selected. + * @return mixed + */ + public function run( ...$args ) { + $settings = $this->get_settings(); + $provider_id = $settings['provider'] ?? NLU::ID; + $provider_instance = $this->get_feature_provider_instance( $provider_id ); + $result = ''; + + if ( NLU::ID === $provider_instance::ID ) { + /** @var NLU $provider_instance */ + $result = call_user_func_array( + [ $provider_instance, 'classify' ], + [ ...$args ] + ); + } elseif ( Embeddings::ID === $provider_instance::ID ) { + /** @var Embeddings $provider_instance */ + $result = call_user_func_array( + [ $provider_instance, 'generate_embeddings_for_post' ], + [ ...$args ] + ); + } + + return apply_filters( + 'classifai_' . static::ID . '_run', + $result, + $provider_instance, + $args, + $this + ); + } +} diff --git a/includes/Classifai/Features/ContentResizing.php b/includes/Classifai/Features/ContentResizing.php new file mode 100644 index 000000000..ed0e5303a --- /dev/null +++ b/includes/Classifai/Features/ContentResizing.php @@ -0,0 +1,323 @@ +label = __( 'Content Resizing', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + add_action( + 'admin_footer', + static function () { + if ( + ( isset( $_GET['tab'], $_GET['feature'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + && 'language_processing' === sanitize_text_field( wp_unslash( $_GET['tab'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + && 'feature_content_resizing' === sanitize_text_field( wp_unslash( $_GET['feature'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ) { + printf( + '', + esc_html__( 'Are you sure you want to delete the prompt?', 'classifai' ), + ); + } + } + ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'resize-content', + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'permission_callback' => [ $this, 'resize_content_permissions_check' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Post ID to resize the content for.', 'classifai' ), + ], + 'content' => [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'The content to resize.', 'classifai' ), + ], + 'resize_type' => [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'The type of resize operation. "expand" or "condense".', 'classifai' ), + ], + ], + ] + ); + } + + /** + * Check if a given request has access to resize content. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function resize_content_permissions_check( WP_REST_Request $request ) { + $post_id = $request->get_param( 'id' ); + + // Ensure we have a logged in user that can edit the item. + if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { + return false; + } + + $post_type = get_post_type( $post_id ); + $post_type_obj = get_post_type_object( $post_type ); + + // Ensure the post type is allowed in REST endpoints. + if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) { + return false; + } + + // Ensure the feature is enabled. Also runs a user check. + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Content resizing is not currently enabled.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/resize-content' ) === 0 ) { + return rest_ensure_response( + $this->run( + $request->get_param( 'id' ), + 'resize_content', + [ + 'content' => $request->get_param( 'content' ), + 'resize_type' => $request->get_param( 'resize_type' ), + ] + ) + ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Enqueue the editor scripts. + */ + public function enqueue_editor_assets() { + global $post; + + if ( empty( $post ) || ! is_admin() ) { + return; + } + + wp_enqueue_script( + 'classifai-content-resizing-plugin-js', + CLASSIFAI_PLUGIN_URL . 'dist/content-resizing-plugin.js', + get_asset_info( 'content-resizing-plugin', 'dependencies' ), + get_asset_info( 'content-resizing-plugin', 'version' ), + true + ); + + wp_enqueue_style( + 'classifai-content-resizing-plugin-css', + CLASSIFAI_PLUGIN_URL . 'dist/content-resizing-plugin.css', + [], + get_asset_info( 'content-resizing-plugin', 'version' ), + 'all' + ); + } + + /** + * Enqueue the admin scripts. + * + * @param string $hook_suffix The current admin page. + */ + public function enqueue_admin_assets( string $hook_suffix ) { + // Load asset in new post and edit post screens. + if ( 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix ) { + wp_enqueue_style( + 'classifai-language-processing-style', + CLASSIFAI_PLUGIN_URL . 'dist/language-processing.css', + [], + get_asset_info( 'language-processing', 'version' ), + ); + } + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( '"Condense this text" and "Expand this text" menu items will be added to the paragraph block\'s toolbar menu.', 'classifai' ); + } + + /** + * Add any needed custom fields. + */ + public function add_custom_settings_fields() { + $settings = $this->get_settings(); + + add_settings_field( + 'number_of_suggestions', + esc_html__( 'Number of suggestions', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'number_of_suggestions', + 'input_type' => 'number', + 'min' => 1, + 'step' => 1, + 'default_value' => $settings['number_of_suggestions'], + 'description' => esc_html__( 'Number of suggestions that will be generated in one request.', 'classifai' ), + ] + ); + + add_settings_field( + 'condense_text_prompt', + esc_html__( 'Condense text prompt', 'classifai' ), + [ $this, 'render_prompt_repeater_field' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'condense_text_prompt', + 'placeholder' => esc_html__( 'Decrease the content length no more than 2 to 4 sentences.', 'classifai' ), + 'default_value' => $settings['condense_text_prompt'], + 'description' => esc_html__( 'Enter your custom prompt.', 'classifai' ), + ] + ); + + add_settings_field( + 'expand_text_prompt', + esc_html__( 'Expand text prompt', 'classifai' ), + [ $this, 'render_prompt_repeater_field' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'expand_text_prompt', + 'placeholder' => esc_html__( 'Increase the content length no more than 2 to 4 sentences.', 'classifai' ), + 'default_value' => $settings['expand_text_prompt'], + 'description' => esc_html__( 'Enter your custom prompt.', 'classifai' ), + ] + ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'number_of_suggestions' => 1, + 'condense_text_prompt' => [ + [ + 'title' => esc_html__( 'ClassifAI default', 'classifai' ), + 'prompt' => $this->condense_prompt, + 'original' => 1, + ], + ], + 'expand_text_prompt' => [ + [ + 'title' => esc_html__( 'ClassifAI default', 'classifai' ), + 'prompt' => $this->expand_prompt, + 'original' => 1, + ], + ], + 'provider' => ChatGPT::ID, + ]; + } + + /** + * Sanitizes the default feature settings. + * + * @param array $new_settings Settings being saved. + * @return array + */ + public function sanitize_default_feature_settings( array $new_settings ): array { + $settings = $this->get_settings(); + + $new_settings['number_of_suggestions'] = sanitize_number_of_responses_field( 'number_of_suggestions', $new_settings, $settings ); + $new_settings['condense_text_prompt'] = sanitize_prompts( 'condense_text_prompt', $new_settings ); + $new_settings['expand_text_prompt'] = sanitize_prompts( 'expand_text_prompt', $new_settings ); + + return $new_settings; + } +} diff --git a/includes/Classifai/Features/DescriptiveTextGenerator.php b/includes/Classifai/Features/DescriptiveTextGenerator.php new file mode 100644 index 000000000..dd88dd633 --- /dev/null +++ b/includes/Classifai/Features/DescriptiveTextGenerator.php @@ -0,0 +1,393 @@ +label = __( 'Descriptive Text Generator', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( ImageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_rescan_image' ] ); + + add_filter( 'attachment_fields_to_edit', [ $this, 'add_rescan_button_to_media_modal' ], 10, 2 ); + add_filter( 'wp_generate_attachment_metadata', [ $this, 'generate_image_alt_tags' ], 8, 2 ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'alt-tags/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Image ID to generate alt text for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'descriptive_text_generator_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate descriptive text. + * + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + public function descriptive_text_generator_permissions_check( WP_REST_Request $request ) { + $attachment_id = $request->get_param( 'id' ); + $post_type = get_post_type_object( 'attachment' ); + + // Ensure attachments are allowed in REST endpoints. + if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) { + return false; + } + + // Ensure we have a logged in user that can upload and change files. + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( 'upload_files' ) ) { + return false; + } + + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Image descriptive text is disabled. Please check your settings.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/alt-tags' ) === 0 ) { + $result = $this->run( $request->get_param( 'id' ), 'descriptive_text' ); + + if ( $result && ! is_wp_error( $result ) ) { + $this->save( $result, $request->get_param( 'id' ) ); + } + + return rest_ensure_response( $result ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Generate the alt tags for the image being uploaded. + * + * @param array $metadata The metadata for the image. + * @param int $attachment_id Post ID for the attachment. + * @return array + */ + public function generate_image_alt_tags( array $metadata, int $attachment_id ): array { + if ( ! $this->is_feature_enabled() ) { + return $metadata; + } + + $result = $this->run( $attachment_id, 'descriptive_text' ); + + if ( $result && ! is_wp_error( $result ) ) { + $this->save( $result, $attachment_id ); + } + + return $metadata; + } + + /** + * Save the returned result based on our settings. + * + * @param string $result The result to save. + * @param int $attachment_id The attachment ID. + */ + public function save( string $result, int $attachment_id ) { + $enabled_fields = $this->get_alt_text_settings(); + + if ( in_array( 'alt', $enabled_fields, true ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $result ) ); + } + + $excerpt = get_the_excerpt( $attachment_id ); + + if ( in_array( 'caption', $enabled_fields, true ) && $excerpt !== $result ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_excerpt' => sanitize_text_field( $result ), + ) + ); + } + + $content = get_the_content( null, false, $attachment_id ); + + if ( in_array( 'description', $enabled_fields, true ) && $content !== $result ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_content' => sanitize_text_field( $result ), + ) + ); + } + } + + /** + * Adds a meta box for rescanning options if the settings are configured. + * + * @param \WP_Post $post The post object. + */ + public function setup_attachment_meta_box( \WP_Post $post ) { + if ( ! wp_attachment_is_image( $post ) || ! $this->is_feature_enabled() ) { + return; + } + + // Add our content to the metabox. + add_action( 'classifai_render_attachment_metabox', [ $this, 'attachment_data_meta_box_content' ] ); + + // If the metabox was already registered, don't add it again. + if ( isset( $wp_meta_boxes['attachment']['side']['high']['classifai_image_processing'] ) ) { + return; + } + + // Register the metabox if needed. + add_meta_box( + 'classifai_image_processing', + __( 'ClassifAI Image Processing', 'classifai' ), + [ $this, 'attachment_data_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Render the meta box. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box( \WP_Post $post ) { + /** + * Allows more fields to be rendered in attachment metabox. + * + * @since 3.0.0 + * @hook classifai_render_attachment_metabox + * + * @param {WP_Post} $post The post object. + * @param {object} $this The Provider object. + */ + do_action( 'classifai_render_attachment_metabox', $post, $this ); + } + + /** + * Display meta data. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box_content( \WP_Post $post ) { + $captions = get_post_meta( $post->ID, '_wp_attachment_image_alt', true ) ? __( 'No descriptive text? Rescan image', 'classifai' ) : __( 'Generate descriptive text', 'classifai' ); + ?> + + is_feature_enabled() && ! empty( $this->get_alt_text_settings() ) ) : ?> +
+ +
+ run( $attachment_id, 'descriptive_text' ); + + if ( $result && ! is_wp_error( $result ) ) { + $this->save( $result, $attachment_id ); + } + } + } + + /** + * Adds the rescan buttons to the media modal. + * + * @param array $form_fields Array of fields + * @param \WP_Post $post Post object for the attachment being viewed. + * @return array + */ + public function add_rescan_button_to_media_modal( array $form_fields, \WP_Post $post ): array { + if ( + ! $this->is_feature_enabled() || + ! wp_attachment_is_image( $post ) || + empty( $this->get_alt_text_settings() ) + ) { + return $form_fields; + } + + $alt_tags_text = empty( get_post_meta( $post->ID, '_wp_attachment_image_alt', true ) ) ? __( 'Generate', 'classifai' ) : __( 'Rescan', 'classifai' ); + + $form_fields['rescan_alt_tags'] = [ + 'label' => __( 'Descriptive text', 'classifai' ), + 'input' => 'html', + 'show_in_edit' => false, + 'html' => '', + ]; + + return $form_fields; + } + + /** + * Returns an array of fields enabled to be set to store image captions. + * + * @return array + */ + public function get_alt_text_settings(): array { + $settings = $this->get_settings(); + $enabled_fields = array(); + + if ( ! isset( $settings['descriptive_text_fields'] ) ) { + return array(); + } + + if ( ! is_array( $settings['descriptive_text_fields'] ) ) { + return array( + 'alt' => 'no' === $settings['descriptive_text_fields']['caption'] ? 0 : 'alt', + 'caption' => 0, + 'description' => 0, + ); + } + + foreach ( $settings['descriptive_text_fields'] as $key => $value ) { + if ( 0 !== $value && '0' !== $value ) { + $enabled_fields[] = $key; + } + } + + return $enabled_fields; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'Enable this to generate descriptive text for images.', 'classifai' ); + } + + /** + * Add any needed custom fields. + */ + public function add_custom_settings_fields() { + $settings = $this->get_settings(); + $checkbox_options = array( + 'alt' => esc_html__( 'Alt text', 'classifai' ), + 'caption' => esc_html__( 'Image caption', 'classifai' ), + 'description' => esc_html__( 'Image description', 'classifai' ), + ); + + add_settings_field( + 'descriptive_text_fields', + esc_html__( 'Descriptive text fields', 'classifai' ), + [ $this, 'render_checkbox_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'descriptive_text_fields', + 'options' => $checkbox_options, + 'default_values' => $settings['descriptive_text_fields'], + 'description' => __( 'Choose image fields where the generated text should be applied.', 'classifai' ), + ] + ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'descriptive_text_fields' => [ + 'alt' => 0, + 'caption' => 0, + 'description' => 0, + ], + 'provider' => ComputerVision::ID, + ]; + } + + /** + * Sanitizes the default feature settings. + * + * @param array $new_settings Settings being saved. + * @return array + */ + public function sanitize_default_feature_settings( array $new_settings ): array { + $settings = $this->get_settings(); + + $new_settings['descriptive_text_fields'] = array_map( 'sanitize_text_field', $new_settings['descriptive_text_fields'] ?? $settings['descriptive_text_fields'] ); + + return $new_settings; + } +} diff --git a/includes/Classifai/Features/ExcerptGeneration.php b/includes/Classifai/Features/ExcerptGeneration.php new file mode 100644 index 000000000..2add6f960 --- /dev/null +++ b/includes/Classifai/Features/ExcerptGeneration.php @@ -0,0 +1,376 @@ +label = __( 'Excerpt Generation', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + add_action( + 'admin_footer', + static function () { + if ( + ( isset( $_GET['tab'], $_GET['feature'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + && 'language_processing' === sanitize_text_field( wp_unslash( $_GET['tab'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + && 'feature_excerpt_generation' === sanitize_text_field( wp_unslash( $_GET['feature'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ) { + printf( + '', + esc_html__( 'Are you sure you want to delete the prompt?', 'classifai' ), + ); + } + } + ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'generate-excerpt(?:/(?P\d+))?', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Post ID to generate excerpt for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'generate_excerpt_permissions_check' ], + ], + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'content' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Content to summarize into an excerpt.', 'classifai' ), + ], + 'title' => [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Title of content we want a summary for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'generate_excerpt_permissions_check' ], + ], + ] + ); + } + + /** + * Check if a given request has access to generate an excerpt. + * + * This check ensures we have a proper post ID, the current user + * making the request has access to that post, that we are + * properly authenticated with OpenAI and that excerpt generation + * is turned on. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function generate_excerpt_permissions_check( WP_REST_Request $request ) { + $post_id = $request->get_param( 'id' ); + + // Ensure we have a logged in user that can edit the item. + if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { + return false; + } + + $post_type = get_post_type( $post_id ); + $post_type_obj = get_post_type_object( $post_type ); + + // Ensure the post type is allowed in REST endpoints. + if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) { + return false; + } + + // Ensure the feature is enabled. Also runs a user check. + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Excerpt generation not currently enabled.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/generate-excerpt' ) === 0 ) { + return rest_ensure_response( + $this->run( + $request->get_param( 'id' ), + 'excerpt', + [ + 'content' => $request->get_param( 'content' ), + 'title' => $request->get_param( 'title' ), + ] + ) + ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Enqueue the editor scripts. + */ + public function enqueue_editor_assets() { + global $post; + + if ( empty( $post ) || ! is_admin() ) { + return; + } + + // This script removes the core excerpt panel and replaces it with our own. + wp_enqueue_script( + 'classifai-post-excerpt', + CLASSIFAI_PLUGIN_URL . 'dist/post-excerpt.js', + array_merge( get_asset_info( 'post-excerpt', 'dependencies' ), [ 'lodash' ] ), + get_asset_info( 'post-excerpt', 'version' ), + true + ); + } + + /** + * Enqueue the admin scripts. + * + * @param string $hook_suffix The current admin page. + */ + public function enqueue_admin_assets( string $hook_suffix ) { + // Load asset in new post and edit post screens. + if ( 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix ) { + $screen = get_current_screen(); + + // Load the assets for the classic editor. + if ( $screen && ! $screen->is_block_editor() ) { + if ( post_type_supports( $screen->post_type, 'excerpt' ) ) { + wp_enqueue_style( + 'classifai-generate-title-classic-css', + CLASSIFAI_PLUGIN_URL . 'dist/generate-title-classic.css', + [], + get_asset_info( 'generate-title-classic', 'version' ), + 'all' + ); + + wp_enqueue_script( + 'classifai-generate-excerpt-classic-js', + CLASSIFAI_PLUGIN_URL . 'dist/generate-excerpt-classic.js', + array_merge( get_asset_info( 'generate-excerpt-classic', 'dependencies' ), array( 'wp-api' ) ), + get_asset_info( 'generate-excerpt-classic', 'version' ), + true + ); + + wp_add_inline_script( + 'classifai-generate-excerpt-classic-js', + sprintf( + 'var classifaiGenerateExcerpt = %s;', + wp_json_encode( + [ + 'path' => '/classifai/v1/generate-excerpt/', + 'buttonText' => __( 'Generate excerpt', 'classifai' ), + 'regenerateText' => __( 'Re-generate excerpt', 'classifai' ), + ] + ) + ), + 'before' + ); + } + } + + wp_enqueue_style( + 'classifai-language-processing-style', + CLASSIFAI_PLUGIN_URL . 'dist/language-processing.css', + [], + get_asset_info( 'language-processing', 'version' ), + ); + } + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'A button will be added to the status panel that can be used to generate titles.', 'classifai' ); + } + + /** + * Add any needed custom fields. + */ + public function add_custom_settings_fields() { + $settings = $this->get_settings(); + $post_types = \Classifai\get_post_types_for_language_settings(); + $post_type_options = array(); + + foreach ( $post_types as $post_type ) { + if ( post_type_supports( $post_type->name, 'excerpt' ) ) { + $post_type_options[ $post_type->name ] = $post_type->label; + } + } + + add_settings_field( + 'generate_excerpt_prompt', + esc_html__( 'Prompt', 'classifai' ), + [ $this, 'render_prompt_repeater_field' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'generate_excerpt_prompt', + 'placeholder' => $this->prompt, + 'default_value' => $settings['generate_excerpt_prompt'], + 'description' => esc_html__( "Add a custom prompt. Note the following variables that can be used in the prompt and will be replaced with content: {{WORDS}} will be replaced with the desired excerpt length setting. {{TITLE}} will be replaced with the item's title.", 'classifai' ), + ] + ); + + add_settings_field( + 'post_types', + esc_html__( 'Allowed post types', 'classifai' ), + [ $this, 'render_checkbox_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'post_types', + 'options' => $post_type_options, + 'default_values' => $settings['post_types'], + 'description' => __( 'Choose which post types support this feature.', 'classifai' ), + ] + ); + + add_settings_field( + 'length', + esc_html__( 'Excerpt length', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'length', + 'input_type' => 'number', + 'min' => 1, + 'step' => 1, + 'default_value' => $settings['length'], + 'description' => __( 'How many words should the excerpt be? Note that the final result may not exactly match this. In testing, ChatGPT tended to exceed this number by 10-15 words.', 'classifai' ), + ] + ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'generate_excerpt_prompt' => [ + [ + 'title' => esc_html__( 'ClassifAI default', 'classifai' ), + 'prompt' => $this->prompt, + 'original' => 1, + ], + ], + 'post_types' => [], + 'length' => absint( apply_filters( 'excerpt_length', 55 ) ), + 'provider' => ChatGPT::ID, + ]; + } + + /** + * Sanitizes the default feature settings. + * + * @param array $new_settings Settings being saved. + * @return array + */ + public function sanitize_default_feature_settings( array $new_settings ): array { + $settings = $this->get_settings(); + $post_types = \Classifai\get_post_types_for_language_settings(); + + $new_settings['generate_excerpt_prompt'] = sanitize_prompts( 'generate_excerpt_prompt', $new_settings ); + + $new_settings['length'] = absint( $settings['length'] ?? $new_settings['length'] ); + + foreach ( $post_types as $post_type ) { + if ( ! post_type_supports( $post_type->name, 'excerpt' ) ) { + continue; + } + + if ( ! isset( $new_settings['post_types'][ $post_type->name ] ) ) { + $new_settings['post_types'][ $post_type->name ] = $settings['post_types']; + } else { + $new_settings['post_types'][ $post_type->name ] = sanitize_text_field( $new_settings['post_types'][ $post_type->name ] ); + } + } + + return $new_settings; + } +} diff --git a/includes/Classifai/Features/Feature.php b/includes/Classifai/Features/Feature.php new file mode 100644 index 000000000..7f11af94e --- /dev/null +++ b/includes/Classifai/Features/Feature.php @@ -0,0 +1,1263 @@ +is_feature_enabled() ) { + $this->feature_setup(); + } + } + + /** + * Setup any hooks the feature needs. + * + * Only fires if the feature is enabled. + */ + public function feature_setup() { + } + + /** + * Assigns user roles to the $roles array. + */ + public function setup_roles() { + $default_settings = $this->get_default_settings(); + $this->roles = get_editable_roles() ?? []; + $this->roles = array_combine( array_keys( $this->roles ), array_column( $this->roles, 'name' ) ); + + /** + * Filter the allowed WordPress roles for a feature. + * + * @since 3.0.0 + * @hook classifai_{feature}_roles + * + * @param {array} $roles Array of arrays containing role information. + * @param {array} $default_settings Default setting values. + * + * @return {array} Roles array. + */ + $this->roles = apply_filters( 'classifai_' . static::ID . '_roles', $this->roles, $default_settings ); + } + + /** + * Returns the label of the feature. + * + * @return string + */ + public function get_label(): string { + /** + * Filter the feature label. + * + * @since 3.0.0 + * @hook classifai_{feature}_label + * + * @param {string} $label Feature label. + * + * @return {string} Filtered label. + */ + return apply_filters( + 'classifai_' . static::ID . '_label', + $this->label + ); + } + + /** + * Set up the fields for each section. + * + * @internal + */ + public function setup_fields_sections() { + $settings = $this->get_settings(); + + add_settings_section( + $this->get_option_name() . '_section', + esc_html__( 'Feature settings', 'classifai' ), + '__return_empty_string', + $this->get_option_name() + ); + + // Add the enable field. + add_settings_field( + 'status', + esc_html__( 'Enable feature', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'status', + 'input_type' => 'checkbox', + 'default_value' => $settings['status'], + 'description' => $this->get_enable_description(), + ] + ); + + // Add all the needed provider fields. + $this->add_provider_fields(); + + // Add any needed custom fields. + $this->add_custom_settings_fields(); + + // Add user/role-based access fields. + $this->add_access_control_fields(); + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return ''; + } + + /** + * Add any needed custom fields. + */ + public function add_custom_settings_fields() { + } + + /** + * Returns the default settings for the feature. + * + * The root-level keys are the setting keys that are independent of the provider. + * Provider specific settings should be nested under the provider key. + * + * @internal + * @return array + */ + protected function get_default_settings(): array { + $shared_defaults = [ + 'status' => '0', + 'role_based_access' => '1', + 'roles' => array_combine( array_keys( $this->roles ), array_keys( $this->roles ) ), + 'user_based_access' => 'no', + 'users' => [], + 'user_based_opt_out' => 'no', + ]; + $provider_settings = $this->get_provider_default_settings(); + $feature_settings = $this->get_feature_default_settings(); + + /** + * Filter the default settings for a feature. + * + * @since 3.0.0 + * @hook classifai_{feature}_get_default_settings + * + * @param {array} $defaults Default feature settings. + * + * @return {array} Filtered default feature settings. + */ + return apply_filters( + 'classifai_' . static::ID . '_get_default_settings', + array_merge( + $shared_defaults, + $feature_settings, + $provider_settings + ) + ); + } + + /** + * Sanitizes the settings before saving. + * + * @internal + * @param array $settings The settings to be sanitized on save. + * @return array + */ + public function sanitize_settings( array $settings ): array { + $new_settings = $settings; + $current_settings = $this->get_settings(); + + // Sanitize the shared settings. + $new_settings['status'] = $settings['status'] ?? $current_settings['status']; + $new_settings['provider'] = isset( $settings['provider'] ) ? sanitize_text_field( $settings['provider'] ) : $current_settings['provider']; + + if ( empty( $settings['role_based_access'] ) || 1 !== (int) $settings['role_based_access'] ) { + $new_settings['role_based_access'] = 'no'; + } else { + $new_settings['role_based_access'] = '1'; + } + + // Allowed roles. + if ( isset( $settings['roles'] ) && is_array( $settings['roles'] ) ) { + $new_settings['roles'] = array_map( 'sanitize_text_field', $settings['roles'] ); + } else { + $new_settings['roles'] = $current_settings['roles']; + } + + if ( empty( $settings['user_based_access'] ) || 1 !== (int) $settings['user_based_access'] ) { + $new_settings['user_based_access'] = 'no'; + } else { + $new_settings['user_based_access'] = '1'; + } + + // Allowed users. + if ( isset( $settings['users'] ) && ! empty( $settings['users'] ) ) { + if ( is_array( $settings['users'] ) ) { + $new_settings['users'] = array_map( 'absint', $settings['users'] ); + } else { + $new_settings['users'] = array_map( 'absint', explode( ',', $settings['users'] ) ); + } + } else { + $new_settings['users'] = array(); + } + + // User-based opt-out. + if ( empty( $settings['user_based_opt_out'] ) || 1 !== (int) $settings['user_based_opt_out'] ) { + $new_settings['user_based_opt_out'] = 'no'; + } else { + $new_settings['user_based_opt_out'] = '1'; + } + + // Sanitize the feature specific settings. + $new_settings = $this->sanitize_default_feature_settings( $new_settings ); + + // Sanitize the provider specific settings. + $provider_instance = $this->get_feature_provider_instance( $new_settings['provider'] ); + $new_settings = $provider_instance->sanitize_settings( $new_settings ); + + /** + * Filter to change settings before they're saved. + * + * @since 3.0.0 + * @hook classifai_{$feature}_sanitize_settings + * + * @param {array} $new_settings Settings being saved. + * @param {array} $current_settings Existing settings. + * + * @return {array} Filtered settings. + */ + return apply_filters( + 'classifai_' . static::ID . '_sanitize_settings', + $new_settings, + $current_settings + ); + } + + /** + * Sanitize the default feature settings. + * + * @param array $settings Settings to sanitize. + * @return array + */ + public function sanitize_default_feature_settings( array $settings ): array { + return $settings; + } + + /** + * Registers the settings for the feature. + */ + public function register_setting() { + register_setting( + $this->get_option_name(), + $this->get_option_name(), + [ + 'sanitize_callback' => [ $this, 'sanitize_settings' ], + ] + ); + } + + /** + * Returns the option name for the feature. + * + * @return string + */ + public function get_option_name(): string { + return 'classifai_' . static::ID; + } + + /** + * Returns the settings for the feature. + * + * @param string $index The index of the setting to return. + * @return array|string + */ + public function get_settings( $index = false ) { + $defaults = $this->get_default_settings(); + $settings = get_option( $this->get_option_name(), [] ); + $settings = $this->merge_settings( $settings, $defaults ); + + if ( $index && isset( $settings[ $index ] ) ) { + return $settings[ $index ]; + } + + return $settings; + } + + /** + * Returns the default settings for the provider selected for the feature. + * + * @return array + */ + public function get_provider_default_settings(): array { + $provider_settings = []; + + foreach ( array_keys( $this->get_providers() ) as $provider_id ) { + $provider = $this->get_feature_provider_instance( $provider_id ); + + if ( $provider && method_exists( $provider, 'get_default_provider_settings' ) ) { + $provider_settings[ $provider_id ] = $provider->get_default_provider_settings(); + } + } + + return $provider_settings; + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + abstract public function get_feature_default_settings(): array; + + /** + * Add the provider fields. + * + * Will add a field to choose the provider and any + * fields the selected provider has registered. + */ + public function add_provider_fields() { + $settings = $this->get_settings(); + + add_settings_field( + 'provider', + esc_html__( 'Select a provider', 'classifai' ), + [ $this, 'render_select' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'provider', + 'options' => $this->get_providers(), + 'default_value' => $settings['provider'], + ] + ); + + foreach ( array_keys( $this->get_providers() ) as $provider_id ) { + $provider = $this->get_feature_provider_instance( $provider_id ); + + if ( $provider && method_exists( $provider, 'render_provider_fields' ) ) { + $provider->render_provider_fields(); + } + } + } + + /** + * Merges the data settings with the default settings recursively. + * + * @internal + * + * @param array $settings Settings data from the database. + * @param array $defaults Default feature and providers settings data. + * @return array + */ + protected function merge_settings( array $settings = [], array $defaults = [] ): array { + foreach ( $defaults as $key => $value ) { + if ( ! isset( $settings[ $key ] ) ) { + $settings[ $key ] = $defaults[ $key ]; + } elseif ( is_array( $value ) ) { + $settings[ $key ] = $this->merge_settings( $settings[ $key ], $defaults[ $key ] ); + } + } + + return $settings; + } + + /** + * Returns the providers supported by the feature. + * + * @internal + * @return array + */ + protected function get_providers(): array { + /** + * Filter the feature providers. + * + * @since 3.0.0 + * @hook classifai_{feature}_providers + * + * @param {array} $providers Feature providers. + * + * @return {array} Filtered providers. + */ + return apply_filters( + 'classifai_' . static::ID . '_providers', + $this->supported_providers + ); + } + + /** + * Resets settings for the provider. + */ + public function reset_settings() { + update_option( $this->get_option_name(), $this->get_default_settings() ); + } + + /** + * Add settings fields for Role/User based access. + */ + protected function add_access_control_fields() { + $settings = $this->get_settings(); + + add_settings_field( + 'role_based_access', + esc_html__( 'Enable role-based access', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'role_based_access', + 'input_type' => 'checkbox', + 'default_value' => $settings['role_based_access'], + 'description' => __( 'Enables ability to select which roles can access this feature.', 'classifai' ), + 'class' => 'classifai-role-based-access', + ] + ); + + // Add hidden class if role-based access is disabled. + $class = 'allowed_roles_row'; + if ( ! isset( $settings['role_based_access'] ) || '1' !== $settings['role_based_access'] ) { + $class .= ' hidden'; + } + + add_settings_field( + 'roles', + esc_html__( 'Allowed roles', 'classifai' ), + [ $this, 'render_checkbox_group' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'roles', + 'options' => $this->roles, + 'default_values' => $settings['roles'], + 'description' => __( 'Choose which roles are allowed to access this feature.', 'classifai' ), + 'class' => $class, + ] + ); + + add_settings_field( + 'user_based_access', + esc_html__( 'Enable user-based access', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'user_based_access', + 'input_type' => 'checkbox', + 'default_value' => $settings['user_based_access'], + 'description' => __( 'Enables ability to select which users can access this feature.', 'classifai' ), + 'class' => 'classifai-user-based-access', + ] + ); + + // Add hidden class if user-based access is disabled. + $users_class = 'allowed_users_row'; + if ( ! isset( $settings['user_based_access'] ) || '1' !== $settings['user_based_access'] ) { + $users_class .= ' hidden'; + } + + add_settings_field( + 'users', + esc_html__( 'Allowed users', 'classifai' ), + [ $this, 'render_allowed_users' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'users', + 'default_value' => $settings['users'], + 'description' => __( 'Users who have access to this feature.', 'classifai' ), + 'class' => $users_class, + ] + ); + + add_settings_field( + 'user_based_opt_out', + esc_html__( 'Enable user-based opt-out', 'classifai' ), + [ $this, 'render_input' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'user_based_opt_out', + 'input_type' => 'checkbox', + 'default_value' => $settings['user_based_opt_out'], + 'description' => __( 'Enables ability for users to opt-out from their user profile page.', 'classifai' ), + 'class' => 'classifai-user-based-opt-out', + ] + ); + } + + /** + * Generic text input field callback + * + * @param array $args The args passed to add_settings_field. + */ + public function render_input( array $args ) { + $option_index = isset( $args['option_index'] ) ? $args['option_index'] : false; + $setting_index = $this->get_settings( $option_index ); + $type = $args['input_type'] ?? 'text'; + $value = ( isset( $setting_index[ $args['label_for'] ] ) ) ? $setting_index[ $args['label_for'] ] : ''; + + // Check for a default value + $value = ( empty( $value ) && isset( $args['default_value'] ) ) ? $args['default_value'] : $value; + $attrs = ''; + $class = ''; + + switch ( $type ) { + case 'text': + case 'password': + $attrs = ' value="' . esc_attr( $value ) . '"'; + $class = 'regular-text'; + break; + case 'number': + $attrs = ' value="' . esc_attr( $value ) . '"'; + + if ( isset( $args['max'] ) && is_numeric( $args['max'] ) ) { + $attrs .= ' max="' . esc_attr( (float) $args['max'] ) . '"'; + } + + if ( isset( $args['min'] ) && is_numeric( $args['min'] ) ) { + $attrs .= ' min="' . esc_attr( (float) $args['min'] ) . '"'; + } + + if ( isset( $args['step'] ) && is_numeric( $args['step'] ) ) { + $attrs .= ' step="' . esc_attr( (float) $args['step'] ) . '"'; + } + + $class = 'small-text'; + break; + case 'checkbox': + $attrs = ' value="1"' . checked( '1', $value, false ); + ?> + + + + get_data_attribute( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + /> + + ' . wp_kses_post( $args['description'] ) . ''; + } + } + + /** + * Generic prompt repeater field callback + * + * @since 2.4.0 + * + * @param array $args The args passed to add_settings_field. + */ + public function render_prompt_repeater_field( array $args ) { + $option_index = $args['option_index'] ?? false; + $setting_index = $this->get_settings( $option_index ); + $prompts = $setting_index[ $args['label_for'] ] ?? []; + $class = $args['class'] ?? 'large-text'; + $placeholder = $args['placeholder'] ?? ''; + $field_name_prefix = sprintf( + '%1$s%2$s[%3$s]', + $this->get_option_name(), + $option_index ? "[$option_index]" : '', + $args['label_for'] + ); + + $prompts = empty( $prompts ) && isset( $args['default_value'] ) ? $args['default_value'] : $prompts; + + $prompt_count = count( $prompts ); + $field_index = 0; + ?> + + + + +
+ +

+ ; %2$s with ; %3$s with prompt. */ + esc_html__( '%1$sClassifAI default prompt%2$s: %3$s', 'classifai' ), + '', + '', + esc_html( $placeholder ) + ); + ?> +

+ + + " + value="" + class="js-setting-field__default"> + " + value=""> + + + + + +
+ + + + + + ' . wp_kses_post( $args['description'] ) . ''; + } + } + + /** + * Renders a select menu + * + * @param array $args The args passed to add_settings_field. + */ + public function render_select( array $args ) { + $option_index = isset( $args['option_index'] ) ? $args['option_index'] : false; + $setting_index = $this->get_settings( $option_index ); + $saved = ( isset( $setting_index[ $args['label_for'] ] ) ) ? $setting_index[ $args['label_for'] ] : ''; + + // Check for a default value + $saved = ( empty( $saved ) && isset( $args['default_value'] ) ) ? $args['default_value'] : $saved; + $options = isset( $args['options'] ) ? $args['options'] : []; + ?> + + + + ' . wp_kses_post( $args['description'] ) . ''; + } + } + + /** + * Render a group of checkboxes. + * + * @param array $args The args passed to add_settings_field + */ + public function render_checkbox_group( array $args = array() ) { + $option_index = isset( $args['option_index'] ) ? $args['option_index'] : false; + $setting_index = $this->get_settings(); + + // Iterate through all of our options. + foreach ( $args['options'] as $option_value => $option_label ) { + $value = ''; + $default_key = array_search( $option_value, $args['default_values'], true ); + + // Get saved value, if any. + if ( isset( $setting_index[ $args['label_for'] ] ) ) { + $value = $setting_index[ $args['label_for'] ][ $option_value ] ?? ''; + } + + // If no saved value, check if we have a default value. + if ( empty( $value ) && '0' !== $value && isset( $args['default_values'][ $default_key ] ) ) { + $value = $args['default_values'][ $default_key ]; + } + + // Render checkbox. + printf( + '

+ +

', + esc_attr( $this->get_option_name() ), + $option_index ? '[' . esc_attr( $option_index ) . ']' : '', + esc_attr( $args['label_for'] ), + esc_attr( $option_value ), + checked( $value, $option_value, false ), + esc_html( $option_label ) + ); + } + + // Render description, if any. + if ( ! empty( $args['description'] ) ) { + printf( + '%s', + esc_html( $args['description'] ) + ); + } + } + + /** + * Renders the checkbox group for 'Generate descriptive text' setting. + * + * @param array $args The args passed to add_settings_field. + */ + public function render_auto_caption_fields( array $args ) { + $setting_index = $this->get_settings(); + $default_value = ''; + + if ( isset( $setting_index['enable_image_captions'] ) ) { + if ( ! is_array( $setting_index['enable_image_captions'] ) ) { + if ( '1' === $setting_index['enable_image_captions'] ) { + $default_value = 'alt'; + } elseif ( 'no' === $setting_index['enable_image_captions'] ) { + $default_value = ''; + } + } + } + + $checkbox_options = array( + 'alt' => esc_html__( 'Alt text', 'classifai' ), + 'caption' => esc_html__( 'Image caption', 'classifai' ), + 'description' => esc_html__( 'Image description', 'classifai' ), + ); + + foreach ( $checkbox_options as $option_value => $option_label ) { + if ( isset( $setting_index['enable_image_captions'] ) ) { + if ( ! is_array( $setting_index['enable_image_captions'] ) ) { + $default_value = '1' === $setting_index['enable_image_captions'] ? 'alt' : ''; + } else { + $default_value = $setting_index['enable_image_captions'][ $option_value ]; + } + } + + printf( + '

+ +

', + esc_attr( $this->get_option_name() ), + esc_attr( $args['label_for'] ), + esc_attr( $option_value ), + checked( $default_value, $option_value, false ), + esc_html( $option_label ) + ); + } + + // Render description, if any. + if ( ! empty( $args['description'] ) ) { + printf( + '%s', + esc_html( $args['description'] ) + ); + } + } + + /** + * Render a group of radio. + * + * @param array $args The args passed to add_settings_field + */ + public function render_radio_group( array $args = array() ) { + $option_index = isset( $args['option_index'] ) ? $args['option_index'] : false; + $setting_index = $this->get_settings( $option_index ); + $value = $setting_index[ $args['label_for'] ] ?? ''; + $options = $args['options'] ?? []; + + if ( ! is_array( $options ) ) { + return; + } + + // Iterate through all of our options. + foreach ( $options as $option_value => $option_label ) { + // Render radio button. + printf( + '

+ +

', + esc_attr( $this->get_option_name() ), + $option_index ? '[' . esc_attr( $option_index ) . ']' : '', + esc_attr( $args['label_for'] ), + esc_attr( $option_value ), + checked( $value, $option_value, false ), + esc_html( $option_label ) + ); + } + + // Render description, if any. + if ( ! empty( $args['description'] ) ) { + printf( + '%s', + esc_html( $args['description'] ) + ); + } + } + + /** + * Render allowed users input field. + * + * @param array $args The args passed to add_settings_field + */ + public function render_allowed_users( array $args = array() ) { + $setting_index = $this->get_settings(); + $value = $setting_index[ $args['label_for'] ] ?? array(); + ?> +
+
+ +
+ ' . wp_kses_post( $args['description'] ) . ''; + } + } + + /** + * Determine if the current user has access to the feature + * + * @return bool + */ + public function has_access(): bool { + $access = false; + $user_id = get_current_user_id(); + $user = get_user_by( 'id', $user_id ); + $user_roles = $user->roles ?? []; + $settings = $this->get_settings(); + $feature_roles = $settings['roles'] ?? []; + $feature_users = array_map( 'absint', $settings['users'] ?? [] ); + + $role_based_access_enabled = isset( $settings['role_based_access'] ) && 1 === (int) $settings['role_based_access']; + $user_based_access_enabled = isset( $settings['user_based_access'] ) && 1 === (int) $settings['user_based_access']; + $user_based_opt_out_enabled = isset( $settings['user_based_opt_out'] ) && 1 === (int) $settings['user_based_opt_out']; + + /* + * Checks if Role-based access is enabled and user role has access to the feature. + */ + if ( $role_based_access_enabled ) { + $access = ( ! empty( $feature_roles ) && ! empty( array_intersect( $user_roles, $feature_roles ) ) ); + } + + /* + * Checks if User-based access is enabled and user has access to the feature. + */ + if ( ! $access && $user_based_access_enabled ) { + $access = ( ! empty( $feature_users ) && ! empty( in_array( $user_id, $feature_users, true ) ) ); + } + + /* + * Checks if User-based opt-out is enabled and user has opted out from the feature. + */ + if ( $access && $user_based_opt_out_enabled ) { + $opted_out_features = (array) get_user_meta( $user_id, 'classifai_opted_out_features', true ); + $access = ( ! in_array( static::ID, $opted_out_features, true ) ); + } + + /** + * Filter to override user access to a ClassifAI feature. + * + * @since 3.0.0 + * @hook classifai_{$feature}_has_access + * + * @param {bool} $access Current access value. + * @param {array} $settings Feature settings. + * + * @return {bool} Should the user have access? + */ + return apply_filters( 'classifai_' . static::ID . '_has_access', $access, $settings ); + } + + /** + * Determine if a feature is enabled. + * + * Returns true if the feature meets all the criteria to + * be enabled. False otherwise. + * + * Criteria: + * - Provider is configured. + * - User has access to the feature. + * - Feature is turned on. + * + * @return bool + */ + public function is_feature_enabled(): bool { + $is_feature_enabled = false; + $settings = $this->get_settings(); + + // Check if provider is configured, user has access to the feature and the feature is turned on. + if ( + $this->is_configured() && + $this->has_access() && + $this->is_enabled() + ) { + $is_feature_enabled = true; + } + + /** + * Filter to override permission to a specific classifai feature. + * + * @since 3.0.0 + * @hook classifai_{$feature}_is_feature_enabled + * + * @param {bool} $is_feature_enabled Is the feature enabled? + * @param {array} $settings Current feature settings. + * + * @return {bool} Returns true if the user has access and the feature is enabled, false otherwise. + */ + return apply_filters( 'classifai_' . static::ID . '_is_feature_enabled', $is_feature_enabled, $settings ); + } + + /** + * Determine if the feature is turned on. + * + * Note: This function does not check if the user has access to the feature. + * + * - Use `is_feature_enabled()` to check if the user has access to the feature and feature is turned on. + * - Use `has_access()` to check if the user has access to the feature. + * + * @return bool + */ + public function is_enabled(): bool { + $settings = $this->get_settings(); + + // Check if feature is turned on. + $feature_status = ( isset( $settings['status'] ) && 1 === (int) $settings['status'] ); + $is_configured = $this->is_configured(); + $is_enabled = $feature_status && $is_configured; + + /** + * Filter to override a specific classifai feature enabled. + * + * @since 3.0.0 + * @hook classifai_{$feature}_is_enabled + * + * @param {bool} $is_enabled Is the feature enabled? + * @param {array} $settings Current feature settings. + * + * @return {bool} Returns true if the feature is enabled, false otherwise. + */ + return apply_filters( 'classifai_' . static::ID . '_is_enabled', $is_enabled, $settings ); + } + + /** + * Returns array of instances of provider classes registered for the service. + * + * @internal + * + * @param array $services Array of provider classes. + * @return array + */ + protected function get_provider_instances( array $services ): array { + $provider_instances = []; + + foreach ( $services as $provider_class ) { + $provider_instances[] = new $provider_class( $this ); + } + + return $provider_instances; + } + + /** + * Returns the instance of the provider set for the feature. + * + * @param string $provider_id The ID of the provider. + * @return \Classifai\Providers + */ + public function get_feature_provider_instance( string $provider_id = '' ) { + $provider_id = $provider_id ? $provider_id : $this->get_settings( 'provider' ); + $provider_instance = find_provider_class( $this->provider_instances ?? [], $provider_id ); + + if ( is_wp_error( $provider_instance ) ) { + return null; + } + + $provider_class = get_class( $provider_instance ); + $provider_instance = new $provider_class( $this ); + + return $provider_instance; + } + + /** + * Returns whether the provider is configured or not. + * + * @return bool + */ + public function is_configured(): bool { + $settings = $this->get_settings(); + $provider_id = $settings['provider']; + $is_configured = false; + + if ( ! empty( $settings ) && ! empty( $settings[ $provider_id ]['authenticated'] ) ) { + $is_configured = true; + } + + return $is_configured; + } + + /** + * Can the feature be initialized? + * + * @return bool + */ + public function can_register(): bool { + return $this->is_configured(); + } + + /** + * Get the debug value text. + * + * @param mixed $setting_value The value of the setting. + * @param integer $type The type of debug value to return. + * @return string + */ + public static function get_debug_value_text( $setting_value, $type = 0 ): string { + $debug_value = ''; + + if ( empty( $setting_value ) ) { + $boolean = false; + } elseif ( 'no' === $setting_value ) { + $boolean = false; + } else { + $boolean = true; + } + + switch ( $type ) { + case 0: + $debug_value = $boolean ? __( 'Yes', 'classifai' ) : __( 'No', 'classifai' ); + break; + case 1: + $debug_value = $boolean ? __( 'Enabled', 'classifai' ) : __( 'Disabled', 'classifai' ); + break; + } + + return $debug_value; + } + + /** + * Returns an array of feature-level debug info. + * + * @return array + */ + public function get_debug_information(): array { + $feature_settings = $this->get_settings(); + $provider = $this->get_feature_provider_instance(); + + $roles = array_filter( + $feature_settings['roles'], + function ( $role ) { + return '0' !== $role; + } + ); + + $common_debug_info = [ + __( 'Authenticated', 'classifai' ) => self::get_debug_value_text( $this->is_configured() ), + __( 'Status', 'classifai' ) => self::get_debug_value_text( $feature_settings['status'], 1 ), + __( 'Role-based access', 'classifai' ) => self::get_debug_value_text( $feature_settings['role_based_access'], 1 ), + __( 'Allowed roles (titles)', 'classifai' ) => implode( ', ', $roles ?? [] ), + __( 'User-based access', 'classifai' ) => self::get_debug_value_text( $feature_settings['user_based_access'], 1 ), + __( 'Allowed users (titles)', 'classifai' ) => implode( ', ', $feature_settings['users'] ?? [] ), + __( 'User based opt-out', 'classifai' ) => self::get_debug_value_text( $feature_settings['user_based_opt_out'], 1 ), + __( 'Provider', 'classifai' ) => $feature_settings['provider'], + ]; + + if ( method_exists( $provider, 'get_debug_information' ) ) { + $all_debug_info = array_merge( + $common_debug_info, + $provider->get_debug_information() + ); + } + + /** + * Filter to add feature-level debug information. + * + * @since 3.0.0 + * @hook classifai_{feature}_debug_information + * + * @param {array} $all_debug_info Debug information + * @param {object} $this Current feature class. + * + * @return {array} Returns debug information. + */ + return apply_filters( + 'classifai_' . self::ID . '_debug_information', + $all_debug_info, + $this, + ); + } + + /** + * Returns the data attribute string for an input. + * + * @param array $args The args passed to add_settings_field. + * @return string + */ + protected function get_data_attribute( array $args ): string { + $data_attr = $args['data_attr'] ?? []; + $data_attr_str = ''; + + foreach ( $data_attr as $attr_key => $attr_value ) { + if ( is_scalar( $attr_value ) ) { + $data_attr_str .= 'data-' . $attr_key . '="' . esc_attr( $attr_value ) . '"'; + } else { + $data_attr_str .= 'data-' . $attr_key . '="' . esc_attr( wp_json_encode( $attr_value ) ) . '"'; + } + } + + return $data_attr_str; + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() {} + + /** + * Generic callback that can be used for all custom endpoints. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response|WP_Error + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + return rest_ensure_response( new WP_Error( 'invalid_route', esc_html__( 'Invalid route.', 'classifai' ) ) ); + } + + /** + * Runs the feature. + * + * @param mixed ...$args Arguments required by the feature depending on the provider selected. + * @return mixed + */ + public function run( ...$args ) { + $settings = $this->get_settings(); + $provider_id = $settings['provider']; + $provider_instance = $this->get_feature_provider_instance( $provider_id ); + + if ( ! is_callable( [ $provider_instance, 'rest_endpoint_callback' ] ) ) { + return new WP_Error( 'invalid_route', esc_html__( 'The selected provider does not have a valid callback in place.', 'classifai' ) ); + } + + /** + * Filter the results of running the feature. + * + * @since 3.0.0 + * @hook classifai_{feature}_run + * + * @param {mixed} $result Result of running the feature. + * @param {Classifai\Providers} $provider_instance Provider used. + * @param {mixed} $args Arguments used by the feature. + * @param {Feature} $this Current feature class. + * + * @return {mixed} Results. + */ + return apply_filters( + 'classifai_' . static::ID . '_run', + $provider_instance->rest_endpoint_callback( ...$args ), + $provider_instance, + $args, + $this + ); + } +} diff --git a/includes/Classifai/Features/ImageCropping.php b/includes/Classifai/Features/ImageCropping.php new file mode 100644 index 000000000..777e4d0cd --- /dev/null +++ b/includes/Classifai/Features/ImageCropping.php @@ -0,0 +1,382 @@ +label = __( 'Image Cropping', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( ImageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_crop_image' ] ); + + add_filter( 'attachment_fields_to_edit', [ $this, 'add_rescan_button_to_media_modal' ], 10, 2 ); + add_filter( 'wp_generate_attachment_metadata', [ $this, 'generate_smart_crops' ], 7, 2 ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'smart-crop/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Image ID to generate smart crop.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'smart_crop_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate smart crops. + * + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + public function smart_crop_permissions_check( WP_REST_Request $request ) { + $attachment_id = $request->get_param( 'id' ); + $post_type = get_post_type_object( 'attachment' ); + + // Ensure attachments are allowed in REST endpoints. + if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) { + return false; + } + + // Ensure we have a logged in user that can upload and change files. + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( 'upload_files' ) ) { + return false; + } + + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Smart cropping is disabled. Please check your settings.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/smart-crop' ) === 0 ) { + $result = $this->run( $request->get_param( 'id' ), 'crop' ); + + if ( ! empty( $result ) && ! is_wp_error( $result ) ) { + $meta = $this->save( $result, $request->get_param( 'id' ) ); + wp_update_attachment_metadata( $request->get_param( 'id' ), $meta ); + } + + return rest_ensure_response( $result ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Generate smart cropped thumbnails for the image being uploaded. + * + * @param array $metadata The metadata for the image. + * @param int $attachment_id Post ID for the attachment. + * @return array + */ + public function generate_smart_crops( array $metadata, int $attachment_id ): array { + if ( ! $this->is_feature_enabled() ) { + return $metadata; + } + + $result = $this->run( $attachment_id, 'crop', $metadata ); + + if ( ! empty( $result ) && ! is_wp_error( $result ) ) { + $metadata = $this->save( $result, $attachment_id ); + } + + return $metadata; + } + + /** + * Save the cropped images. + * + * @param array $result The results to save. + * @param int $attachment_id The attachment ID. + * @return array + */ + public function save( array $result, int $attachment_id ): array { + $metadata = wp_get_attachment_metadata( $attachment_id ); + + foreach ( $result as $size => $image ) { + if ( is_wp_error( $image['data'] ) || empty( $image['data'] ) ) { + continue; + } + + $attached_file = get_attached_file( $attachment_id ); + $file_path_info = pathinfo( $attached_file ); + $new_thumb_file_name = str_replace( + $file_path_info['filename'], + sprintf( + '%s-%dx%d', + $file_path_info['filename'], + $image['width'], + $image['height'] + ), + $attached_file + ); + + /** + * Filters the file name of the smart-cropped image. + * + * By default, the filename mirrors what is generated by + * core -- e.g., my-thumb-150x150.jpg -- so will override the + * core-generated image. Apply this filter to keep the original + * file in the file system. + * + * @since 1.5.0 + * @hook classifai_smart_cropping_thumb_file_name + * + * @param {string} Default file name. + * @param {int} The ID of the attachment being processed. + * @param {array} Width and height data for the image. + * + * @return {string} Filtered file name. + */ + $new_thumb_file_name = apply_filters( + 'classifai_smart_cropping_thumb_file_name', + $new_thumb_file_name, + $attachment_id, + [ + 'width' => $image['width'], + 'height' => $image['height'], + ] + ); + + $filesystem = $this->get_wp_filesystem(); + if ( $filesystem && $filesystem->put_contents( $new_thumb_file_name, $image['data'] ) ) { + $metadata['sizes'][ $size ]['file'] = basename( $new_thumb_file_name ); + } + } + + return $metadata; + } + + /** + * Adds a meta box for rescanning options if the settings are configured. + * + * @param \WP_Post $post The post object. + */ + public function setup_attachment_meta_box( \WP_Post $post ) { + if ( ! wp_attachment_is_image( $post ) || ! $this->is_feature_enabled() ) { + return; + } + + // Add our content to the metabox. + add_action( 'classifai_render_attachment_metabox', [ $this, 'attachment_data_meta_box_content' ] ); + + // If the metabox was already registered, don't add it again. + if ( isset( $wp_meta_boxes['attachment']['side']['high']['classifai_image_processing'] ) ) { + return; + } + + // Register the metabox if needed. + add_meta_box( + 'classifai_image_processing', + __( 'ClassifAI Image Processing', 'classifai' ), + [ $this, 'attachment_data_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Render the meta box. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box( \WP_Post $post ) { + /** + * Allows more fields to be rendered in attachment metabox. + * + * @since 3.0.0 + * @hook classifai_render_attachment_metabox + * + * @param {WP_Post} $post The post object. + * @param {object} $this The Provider object. + */ + do_action( 'classifai_render_attachment_metabox', $post, $this ); + } + + /** + * Display meta data. + */ + public function attachment_data_meta_box_content() { + $smart_crop = get_transient( 'classifai_azure_computer_vision_image_cropping_latest_response' ) ? __( 'Regenerate smart thumbnail', 'classifai' ) : __( 'Create smart thumbnail', 'classifai' ); + ?> + + is_feature_enabled() ) : ?> +
+ +
+ run( $attachment_id, 'crop', $metadata ); + + if ( ! empty( $result ) && ! is_wp_error( $result ) ) { + $meta = $this->save( $result, $attachment_id ); + wp_update_attachment_metadata( $attachment_id, $meta ); + } + } + } + + /** + * Adds the rescan buttons to the media modal. + * + * @param array $form_fields Array of fields + * @param \WP_Post $post Post object for the attachment being viewed. + * @return array + */ + public function add_rescan_button_to_media_modal( array $form_fields, \WP_Post $post ): array { + if ( ! $this->is_feature_enabled() || ! wp_attachment_is_image( $post ) ) { + return $form_fields; + } + + $smart_crop_text = empty( get_transient( 'classifai_azure_computer_vision_image_cropping_latest_response' ) ) ? __( 'Generate', 'classifai' ) : __( 'Regenerate', 'classifai' ); + + $form_fields['rescan_smart_crop'] = [ + 'label' => __( 'Smart thumbnail', 'classifai' ), + 'input' => 'html', + 'show_in_edit' => false, + 'html' => '', + ]; + + return $form_fields; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'AI Vision detects and saves the most visually interesting part of your image (i.e., faces, animals, notable text).', 'classifai' ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'provider' => ComputerVision::ID, + ]; + } + + /** + * Provides the global WP_Filesystem_Base class instance. + * + * @return WP_Filesystem_Base + */ + public function get_wp_filesystem() { + global $wp_filesystem; + + if ( is_null( $this->wp_filesystem ) ) { + if ( ! $wp_filesystem ) { + WP_Filesystem(); // Initiates the global. + } + + $this->wp_filesystem = $wp_filesystem; + } + + /** + * Filters the filesystem class instance used to save image files. + * + * @since 1.5.0 + * @hook classifai_smart_crop_wp_filesystem + * + * @param {WP_Filesystem_Base} $this->wp_filesystem Filesystem class for saving images. + * + * @return {WP_Filesystem_Base} Filtered Filesystem class. + */ + return apply_filters( 'classifai_smart_crop_wp_filesystem', $this->wp_filesystem ); + } +} diff --git a/includes/Classifai/Features/ImageGeneration.php b/includes/Classifai/Features/ImageGeneration.php new file mode 100644 index 000000000..a55f39870 --- /dev/null +++ b/includes/Classifai/Features/ImageGeneration.php @@ -0,0 +1,390 @@ +label = __( 'Image Generation', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( ImageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + DallE::ID => __( 'OpenAI Dall-E', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'admin_menu', [ $this, 'register_generate_media_page' ], 0 ); + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); + add_action( 'print_media_templates', [ $this, 'print_media_templates' ] ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'generate-image', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'prompt' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + 'description' => esc_html__( 'Prompt used to generate an image', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'generate_image_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate an image. + * + * This check ensures we have a valid user with proper capabilities + * making the request, that we are properly authenticated with OpenAI + * and that image generation is turned on. + * + * @return WP_Error|bool + */ + public function generate_image_permissions_check() { + // Ensure the feature is enabled. Also runs a user check. + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Image generation not currently enabled.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/generate-image' ) === 0 ) { + return rest_ensure_response( + $this->run( + $request->get_param( 'prompt' ), + 'image_gen', + $request->get_params(), + ) + ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Registers a Media > Generate Image submenu. + */ + public function register_generate_media_page() { + if ( ! $this->is_feature_enabled() ) { + return; + } + + $settings = $this->get_settings(); + $provider_id = $settings['provider']; + $number_of_images = absint( $settings[ $provider_id ]['number_of_images'] ); + + add_submenu_page( + 'upload.php', + $number_of_images > 1 ? esc_html__( 'Generate Images', 'classifai' ) : esc_html__( 'Generate Image', 'classifai' ), + $number_of_images > 1 ? esc_html__( 'Generate Images', 'classifai' ) : esc_html__( 'Generate Image', 'classifai' ), + 'upload_files', + esc_url( admin_url( 'upload.php?action=classifai-generate-image' ) ), + '' + ); + } + + /** + * Enqueue the admin scripts. + * + * @since 2.4.0 Use get_asset_info to get the asset version and dependencies. + * + * @param string $hook_suffix The current admin page. + */ + public function enqueue_admin_scripts( string $hook_suffix = '' ) { + if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix && 'upload.php' !== $hook_suffix ) { + return; + } + + if ( ! $this->is_feature_enabled() ) { + return; + } + + $settings = $this->get_settings(); + $provider_id = $settings['provider']; + $number_of_images = absint( $settings[ $provider_id ]['number_of_images'] ); + + wp_enqueue_media(); + + wp_enqueue_style( + 'classifai-image-processing-style', + CLASSIFAI_PLUGIN_URL . 'dist/media-modal.css', + [], + get_asset_info( 'media-modal', 'version' ), + 'all' + ); + + wp_enqueue_script( + 'classifai-generate-images', + CLASSIFAI_PLUGIN_URL . 'dist/media-modal.js', + array_merge( get_asset_info( 'media-modal', 'dependencies' ), array( 'jquery', 'wp-api' ) ), + get_asset_info( 'media-modal', 'version' ), + true + ); + + wp_enqueue_script( + 'classifai-inserter-media-category', + CLASSIFAI_PLUGIN_URL . 'dist/inserter-media-category.js', + get_asset_info( 'inserter-media-category', 'dependencies' ), + get_asset_info( 'inserter-media-category', 'version' ), + true + ); + + /** + * Filter the default attribution added to generated images. + * + * @since 2.1.0 + * @hook classifai_dalle_caption + * + * @param {string} $caption Attribution to be added as a caption to the image. + * + * @return {string} Caption. + */ + $caption = apply_filters( + 'classifai_dalle_caption', + sprintf( + /* translators: %1$s is replaced with the OpenAI DALL·E URL */ + esc_html__( 'Image generated by OpenAI\'s DALL·E', 'classifai' ), + 'https://openai.com/research/dall-e' + ) + ); + + wp_localize_script( + 'classifai-generate-images', + 'classifaiDalleData', + [ + 'endpoint' => 'classifai/v1/generate-image', + 'tabText' => $number_of_images > 1 ? esc_html__( 'Generate images', 'classifai' ) : esc_html__( 'Generate image', 'classifai' ), + 'errorText' => esc_html__( 'Something went wrong. No results found', 'classifai' ), + 'buttonText' => esc_html__( 'Select image', 'classifai' ), + 'caption' => $caption, + ] + ); + + if ( 'upload.php' === $hook_suffix ) { + $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( 'classifai-generate-image' === $action ) { + wp_enqueue_script( + 'classifai-generate-images-media-upload', + CLASSIFAI_PLUGIN_URL . 'dist/generate-image-media-upload.js', + array_merge( get_asset_info( 'generate-image-media-upload', 'dependencies' ), array( 'jquery' ) ), + get_asset_info( 'classifai-generate-images-media-upload', 'version' ), + true + ); + + wp_localize_script( + 'classifai-generate-images-media-upload', + 'classifaiGenerateImages', + [ + 'upload_url' => esc_url( admin_url( 'upload.php' ) ), + ] + ); + } + } + } + + /** + * Print the templates we need for our media modal integration. + */ + public function print_media_templates() { + if ( ! $this->is_feature_enabled() ) { + return; + } + + $settings = $this->get_settings(); + $provider_id = $settings['provider']; + $number_of_images = absint( $settings[ $provider_id ]['number_of_images'] ); + $provider_instance = $this->get_feature_provider_instance( $provider_id ); + ?> + + + + + + + get_default_settings(); + + // Get all roles that have the upload_files cap. + $roles = get_editable_roles() ?? []; + $roles = array_filter( + $roles, + function ( $role ) { + return isset( $role['capabilities'], $role['capabilities']['upload_files'] ) && $role['capabilities']['upload_files']; + } + ); + $roles = array_combine( array_keys( $roles ), array_column( $roles, 'name' ) ); + + /** + * Filter the allowed WordPress roles for image generation. + * + * @since 2.3.0 + * @hook classifai_feature_image_generation_roles + * + * @param {array} $roles Array of arrays containing role information. + * @param {array} $default_settings Default setting values. + * + * @return {array} Roles array. + */ + $this->roles = apply_filters( 'classifai_' . static::ID . '_roles', $roles, $default_settings ); + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'When enabled, a new Generate images tab will be shown in the media upload flow, allowing you to generate and import images.', 'classifai' ); + } + + /** + * Returns true if the feature meets all the criteria to be enabled. + * + * @return bool + */ + public function is_feature_enabled(): bool { + $settings = $this->get_settings(); + $is_feature_enabled = parent::is_feature_enabled() && current_user_can( 'upload_files' ); + + /** This filter is documented in includes/Classifai/Features/Feature.php */ + return apply_filters( 'classifai_' . static::ID . '_is_feature_enabled', $is_feature_enabled, $settings ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'provider' => DallE::ID, + ]; + } +} diff --git a/includes/Classifai/Features/ImageTagsGenerator.php b/includes/Classifai/Features/ImageTagsGenerator.php new file mode 100644 index 000000000..6a6e02ea0 --- /dev/null +++ b/includes/Classifai/Features/ImageTagsGenerator.php @@ -0,0 +1,357 @@ +label = __( 'Image Tags Generator', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( ImageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_rescan_image' ] ); + + add_filter( 'attachment_fields_to_edit', [ $this, 'add_rescan_button_to_media_modal' ], 10, 2 ); + add_filter( 'wp_generate_attachment_metadata', [ $this, 'generate_image_tags' ], 8, 2 ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'image-tags/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Image ID to generate tags for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'image_tags_generator_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate image tags. + * + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + public function image_tags_generator_permissions_check( WP_REST_Request $request ) { + $attachment_id = $request->get_param( 'id' ); + $post_type = get_post_type_object( 'attachment' ); + + // Ensure attachments are allowed in REST endpoints. + if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) { + return false; + } + + // Ensure we have a logged in user that can upload and change files. + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( 'upload_files' ) ) { + return false; + } + + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Image tagging is disabled. Please check your settings.', 'classifai' ) ); + } + + $settings = $this->get_settings(); + if ( ! empty( $settings ) && isset( $settings['tag_taxonomy'] ) ) { + $permission = check_term_permissions( $settings['tag_taxonomy'] ); + + if ( is_wp_error( $permission ) ) { + return $permission; + } + } else { + return new WP_Error( 'invalid_settings', esc_html__( 'Ensure the service settings have been saved.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/image-tags' ) === 0 ) { + $result = $this->run( $request->get_param( 'id' ), 'tags' ); + + if ( ! empty( $result ) && ! is_wp_error( $result ) ) { + $this->save( $result, $request->get_param( 'id' ) ); + } + + return rest_ensure_response( $result ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Generate the tags for the image being uploaded. + * + * @param array $metadata The metadata for the image. + * @param int $attachment_id Post ID for the attachment. + * @return array + */ + public function generate_image_tags( array $metadata, int $attachment_id ): array { + if ( ! $this->is_feature_enabled() ) { + return $metadata; + } + + $result = $this->run( $attachment_id, 'tags' ); + + if ( ! empty( $result ) && ! is_wp_error( $result ) ) { + $this->save( $result, $attachment_id ); + } + + return $metadata; + } + + /** + * Save the returned result based on our settings. + * + * @param array $result The results to save. + * @param int $attachment_id The attachment ID. + */ + public function save( array $result, int $attachment_id ) { + $settings = $this->get_settings(); + $taxonomy = $settings['tag_taxonomy']; + + foreach ( $result as $tag ) { + wp_add_object_terms( $attachment_id, $tag, $taxonomy ); + } + + wp_update_term_count_now( $result, $taxonomy ); + } + + /** + * Adds a meta box for rescanning options if the settings are configured. + * + * @param \WP_Post $post The post object. + */ + public function setup_attachment_meta_box( \WP_Post $post ) { + global $wp_meta_boxes; + + if ( ! wp_attachment_is_image( $post ) || ! $this->is_feature_enabled() ) { + return; + } + + // Add our content to the metabox. + add_action( 'classifai_render_attachment_metabox', [ $this, 'attachment_data_meta_box_content' ] ); + + // If the metabox was already registered, don't add it again. + if ( isset( $wp_meta_boxes['attachment']['side']['high']['classifai_image_processing'] ) ) { + return; + } + + // Register the metabox if needed. + add_meta_box( + 'classifai_image_processing', + __( 'ClassifAI Image Processing', 'classifai' ), + [ $this, 'attachment_data_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Render the meta box. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box( \WP_Post $post ) { + /** + * Allows more fields to be rendered in attachment metabox. + * + * @since 3.0.0 + * @hook classifai_render_attachment_metabox + * + * @param {WP_Post} $post The post object. + * @param {object} $this The Provider object. + */ + do_action( 'classifai_render_attachment_metabox', $post, $this ); + } + + /** + * Display meta data + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box_content( \WP_Post $post ) { + $tags = ! empty( wp_get_object_terms( $post->ID, 'classifai-image-tags' ) ) ? __( 'Rescan image for new tags', 'classifai' ) : __( 'Generate image tags', 'classifai' ); + ?> + + is_feature_enabled() ) : ?> +
+ +
+ run( $attachment_id, 'tags' ); + + if ( ! empty( $result ) && ! is_wp_error( $result ) ) { + $this->save( $result, $attachment_id ); + } + } + } + + /** + * Adds the rescan buttons to the media modal. + * + * @param array $form_fields Array of fields + * @param \WP_Post $post Post object for the attachment being viewed. + * @return array + */ + public function add_rescan_button_to_media_modal( array $form_fields, \WP_Post $post ): array { + if ( ! $this->is_feature_enabled() || ! wp_attachment_is_image( $post ) ) { + return $form_fields; + } + + $image_tags_text = empty( wp_get_object_terms( $post->ID, 'classifai-image-tags' ) ) ? __( 'Generate', 'classifai' ) : __( 'Rescan', 'classifai' ); + + $form_fields['rescan_captions'] = [ + 'label' => __( 'Image tags', 'classifai' ), + 'input' => 'html', + 'show_in_edit' => false, + 'html' => '', + ]; + + return $form_fields; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'Image tags will be added automatically.', 'classifai' ); + } + + /** + * Add any needed custom fields. + */ + public function add_custom_settings_fields() { + $settings = $this->get_settings(); + $attachment_taxonomies = get_object_taxonomies( 'attachment', 'objects' ); + $options = []; + + foreach ( $attachment_taxonomies as $name => $taxonomy ) { + $options[ $name ] = $taxonomy->label; + } + + add_settings_field( + 'tag_taxonomy', + esc_html__( 'Tag taxonomy', 'classifai' ), + [ $this, 'render_select' ], + $this->get_option_name(), + $this->get_option_name() . '_section', + [ + 'label_for' => 'tag_taxonomy', + 'options' => $options, + 'default_value' => $settings['tag_taxonomy'], + ] + ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + $attachment_taxonomies = get_object_taxonomies( 'attachment', 'objects' ); + $options = []; + + foreach ( $attachment_taxonomies as $name => $taxonomy ) { + $options[ $name ] = $taxonomy->label; + } + + return [ + 'tag_taxonomy' => array_key_first( $options ), + 'provider' => ComputerVision::ID, + ]; + } + + /** + * Sanitizes the default feature settings. + * + * @param array $new_settings Settings being saved. + * @return array + */ + public function sanitize_default_feature_settings( array $new_settings ): array { + $settings = $this->get_settings(); + + $new_settings['tag_taxonomy'] = $new_settings['tag_taxonomy'] ?? $settings['tag_taxonomy']; + + return $new_settings; + } +} diff --git a/includes/Classifai/Features/ImageTextExtraction.php b/includes/Classifai/Features/ImageTextExtraction.php new file mode 100644 index 000000000..90000ebc8 --- /dev/null +++ b/includes/Classifai/Features/ImageTextExtraction.php @@ -0,0 +1,407 @@ +label = __( 'Image Text Extraction', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( ImageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'rest_api_init', [ $this, 'add_ocr_data_to_api_response' ] ); + add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_rescan_image' ] ); + + add_filter( 'the_content', [ $this, 'add_ocr_aria_describedby' ] ); + add_filter( 'attachment_fields_to_edit', [ $this, 'add_rescan_button_to_media_modal' ], 10, 2 ); + add_filter( 'wp_generate_attachment_metadata', [ $this, 'generate_ocr_text' ], 9, 2 ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'ocr/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Image ID to read text from.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'image_text_extractor_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate OCR. + * + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + public function image_text_extractor_permissions_check( WP_REST_Request $request ) { + $attachment_id = $request->get_param( 'id' ); + $post_type = get_post_type_object( 'attachment' ); + + // Ensure attachments are allowed in REST endpoints. + if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) { + return false; + } + + // Ensure we have a logged in user that can upload and change files. + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( 'upload_files' ) ) { + return false; + } + + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Scan image for text is disabled. Please check your settings.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/ocr' ) === 0 ) { + $result = $this->run( $request->get_param( 'id' ), 'ocr' ); + + if ( $result && ! is_wp_error( $result ) ) { + $this->save( $result, $request->get_param( 'id' ) ); + } + + return rest_ensure_response( $result ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Generate the tags for the image being uploaded. + * + * @param array $metadata The metadata for the image. + * @param int $attachment_id Post ID for the attachment. + * @return array + */ + public function generate_ocr_text( array $metadata, int $attachment_id ): array { + if ( ! $this->is_feature_enabled() ) { + return $metadata; + } + + $result = $this->run( $attachment_id, 'ocr' ); + + if ( $result && ! is_wp_error( $result ) ) { + $this->save( $result, $attachment_id ); + } + + return $metadata; + } + + /** + * Save the OCR text. + * + * @param string $result The result to save. + * @param int $attachment_id The attachment ID. + */ + public function save( string $result, int $attachment_id ) { + $content = get_the_content( null, false, $attachment_id ); + + $post_args = [ + 'ID' => $attachment_id, + 'post_content' => sanitize_text_field( $result ), + ]; + + /** + * Filter the post arguments before saving the text to post_content. + * + * This enables text to be stored in a different post or post meta field, + * or do other post data setting based on scan results. + * + * @since 1.6.0 + * @hook classifai_ocr_text_post_args + * + * @param {string} $post_args Array of post data for the attachment post update. Defaults to `ID` and `post_content`. + * @param {int} $attachment_id ID of the attachment post. + * @param {object} $scan The full scan results from the API. + * @param {string} $text The text data to be saved. + * + * @return {string} The filtered text data. + */ + $post_args = apply_filters( 'classifai_ocr_text_post_args', $post_args, $attachment_id, $result ); + + if ( $content !== $result ) { + wp_update_post( $post_args ); + } + } + + /** + * Include classifai_computer_vision_ocr in API response. + */ + public function add_ocr_data_to_api_response() { + register_rest_field( + 'attachment', + 'classifai_has_ocr', + [ + 'get_callback' => function ( $params ) { + return ! empty( get_post_meta( $params['id'], 'classifai_computer_vision_ocr', true ) ); + }, + 'schema' => [ + 'type' => 'boolean', + 'context' => [ 'view' ], + ], + ] + ); + } + + /** + * Enqueue the editor scripts. + */ + public function enqueue_editor_assets() { + wp_enqueue_script( + 'editor-ocr', + CLASSIFAI_PLUGIN_URL . 'dist/editor-ocr.js', + array_merge( get_asset_info( 'editor-ocr', 'dependencies' ), array( 'lodash' ) ), + get_asset_info( 'editor-ocr', 'version' ), + true + ); + } + + /** + * Adds a meta box for rescanning options if the settings are configured. + * + * @param \WP_Post $post The post object. + */ + public function setup_attachment_meta_box( \WP_Post $post ) { + if ( ! wp_attachment_is_image( $post ) || ! $this->is_feature_enabled() ) { + return; + } + + // Add our content to the metabox. + add_action( 'classifai_render_attachment_metabox', [ $this, 'attachment_data_meta_box_content' ] ); + + // If the metabox was already registered, don't add it again. + if ( isset( $wp_meta_boxes['attachment']['side']['high']['classifai_image_processing'] ) ) { + return; + } + + // Register the metabox if needed. + add_meta_box( + 'classifai_image_processing', + __( 'ClassifAI Image Processing', 'classifai' ), + [ $this, 'attachment_data_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Render the meta box. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box( \WP_Post $post ) { + /** + * Allows more fields to be rendered in attachment metabox. + * + * @since 3.0.0 + * @hook classifai_render_attachment_metabox + * + * @param {WP_Post} $post The post object. + * @param {object} $this The Provider object. + */ + do_action( 'classifai_render_attachment_metabox', $post, $this ); + } + + /** + * Display meta data. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box_content( \WP_Post $post ) { + $ocr = get_post_meta( $post->ID, 'classifai_computer_vision_ocr', true ) ? __( 'Rescan for text', 'classifai' ) : __( 'Scan image for text', 'classifai' ); + ?> + + is_feature_enabled() ) : ?> +
+ +
+ run( $attachment_id, 'ocr' ); + + if ( $result && ! is_wp_error( $result ) ) { + $this->save( $result, $attachment_id ); + } + } + } + + /** + * Filter the post content to inject aria-describedby attribute. + * + * @param string $content Post content. + * @return string + */ + public function add_ocr_aria_describedby( string $content ): string { + $modified = false; + + if ( ! is_singular() || empty( $content ) ) { + return $content; + } + + $dom = new DOMDocument(); + + // Suppress warnings generated by loadHTML. + $errors = libxml_use_internal_errors( true ); + $dom->loadHTML( + sprintf( + '%s', + esc_attr( get_bloginfo( 'charset' ) ), + $content + ) + ); + libxml_use_internal_errors( $errors ); + + foreach ( $dom->getElementsByTagName( 'img' ) as $image ) { + foreach ( $image->attributes as $attribute ) { + if ( 'aria-describedby' === $attribute->name ) { + break; + } + + if ( 'class' !== $attribute->name ) { + continue; + } + + $image_id = preg_match( '~wp-image-\K\d+~', $image->getAttribute( 'class' ), $out ) ? $out[0] : 0; + $ocr_scanned_text_id = "classifai-ocr-$image_id"; + $ocr_scanned_text = $dom->getElementById( $ocr_scanned_text_id ); + + if ( ! empty( $ocr_scanned_text ) ) { + $image->setAttribute( 'aria-describedby', $ocr_scanned_text_id ); + $modified = true; + } + } + } + + if ( $modified ) { + $body = $dom->getElementsByTagName( 'body' )->item( 0 ); + return trim( $dom->saveHTML( $body ) ); + } + + return $content; + } + + /** + * Adds the rescan buttons to the media modal. + * + * @param array $form_fields Array of fields + * @param \WP_Post $post Post object for the attachment being viewed. + * @return array + */ + public function add_rescan_button_to_media_modal( array $form_fields, \WP_Post $post ): array { + if ( ! $this->is_feature_enabled() || ! wp_attachment_is_image( $post ) ) { + return $form_fields; + } + + $ocr_text = empty( get_post_meta( $post->ID, 'classifai_computer_vision_ocr', true ) ) ? __( 'Scan', 'classifai' ) : __( 'Rescan', 'classifai' ); + + $form_fields['rescan_ocr'] = [ + 'label' => __( 'Scan image for text', 'classifai' ), + 'input' => 'html', + 'show_in_edit' => false, + 'html' => '', + ]; + + return $form_fields; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'OCR detects text in images (e.g., handwritten notes) and saves that as post content.', 'classifai' ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'provider' => ComputerVision::ID, + ]; + } +} diff --git a/includes/Classifai/Features/PDFTextExtraction.php b/includes/Classifai/Features/PDFTextExtraction.php new file mode 100644 index 000000000..3baa6bccb --- /dev/null +++ b/includes/Classifai/Features/PDFTextExtraction.php @@ -0,0 +1,272 @@ +label = __( 'PDF Text Extraction', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( ImageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + ComputerVision::ID => __( 'Microsoft Azure AI Vision', 'classifai' ), + ]; + } + + /** + * Set up necessary hooks. + * + * We utilize this so we can register the REST route. + */ + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } + + /** + * Set up necessary hooks. + */ + public function feature_setup() { + add_action( 'add_meta_boxes_attachment', [ $this, 'setup_attachment_meta_box' ] ); + add_action( 'add_attachment', [ $this, 'read_pdf' ] ); + add_action( 'edit_attachment', [ $this, 'maybe_rescan_pdf' ] ); + + add_filter( 'attachment_fields_to_edit', [ $this, 'add_rescan_button_to_media_modal' ], 10, 2 ); + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'read-pdf/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'rest_endpoint_callback' ], + 'args' => [ + 'id' => [ + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'Image ID to generate alt text for.', 'classifai' ), + ], + ], + 'permission_callback' => [ $this, 'read_pdf_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to read a PDF. + * + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + public function read_pdf_permissions_check( WP_REST_Request $request ) { + $attachment_id = $request->get_param( 'id' ); + $post_type = get_post_type_object( 'attachment' ); + + // Ensure attachments are allowed in REST endpoints. + if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) { + return false; + } + + // Ensure we have a logged in user that can upload and change files. + if ( empty( $attachment_id ) || ! current_user_can( 'edit_post', $attachment_id ) || ! current_user_can( 'upload_files' ) ) { + return false; + } + + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'PDF Text Extraction is disabled. Please check your settings.', 'classifai' ) ); + } + + return true; + } + + /** + * Generic request handler for all our custom routes. + * + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response + */ + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/read-pdf' ) === 0 ) { + return rest_ensure_response( + $this->run( $request->get_param( 'id' ), 'read_pdf' ) + ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Adds a meta box for rescanning options if the settings are configured. + * + * @param \WP_Post $post The post object. + */ + public function setup_attachment_meta_box( \WP_Post $post ) { + if ( ! attachment_is_pdf( $post ) || ! $this->is_feature_enabled() ) { + return; + } + + add_meta_box( + 'classifai_pdf_processing', + __( 'ClassifAI PDF Processing', 'classifai' ), + [ $this, 'attachment_data_meta_box' ], + 'attachment', + 'side', + 'high' + ); + } + + /** + * Render the meta box. + * + * @param \WP_Post $post The post object. + */ + public function attachment_data_meta_box( \WP_Post $post ) { + /** + * Filter the status of the PDF read operation. + * + * @since 3.0.0 + * @hook classifai_feature_pdf_to_text_generation_read_status + * + * @param {array} $status Status of the PDF read operation. + * @param {int} $post_id ID of attachment. + * + * @return {array} Status. + */ + $status = apply_filters( 'classifai_' . static::ID . '_read_status', [], $post->ID ); + + $read = ! empty( $status['read'] ) && (bool) $status['read'] ? __( 'Rescan PDF for text', 'classifai' ) : __( 'Scan PDF for text', 'classifai' ); + $running = ! empty( $status['running'] ) && (bool) $status['running']; + ?> + +
+
+ +
+
+ + run( $attachment_id, 'read_pdf' ); + } + + /** + * Determine if we need to rescan the PDF. + * + * @param int $attachment_id Attachment ID. + */ + public function maybe_rescan_pdf( int $attachment_id ) { + if ( clean_input( 'rescan-pdf' ) ) { + $this->run( $attachment_id, 'read_pdf' ); + } + } + + /** + * Save the returned result. + * + * @param string $result The result to save. + * @param int $attachment_id The attachment ID. + */ + public function save( string $result, int $attachment_id ) { + return wp_update_post( + [ + 'ID' => $attachment_id, + 'post_content' => $result, + ] + ); + } + + /** + * Adds the rescan buttons to the media modal. + * + * @param array $form_fields Array of fields + * @param \WP_Post $post Post object for the attachment being viewed. + * @return array + */ + public function add_rescan_button_to_media_modal( array $form_fields, \WP_Post $post ): array { + if ( ! $this->is_feature_enabled() || ! attachment_is_pdf( $post ) ) { + return $form_fields; + } + + $read_text = empty( get_the_content( null, false, $post ) ) ? __( 'Scan', 'classifai' ) : __( 'Rescan', 'classifai' ); + $status = apply_filters( 'classifai_' . static::ID . '_read_status', [], $post->ID ); + + if ( ! empty( $status['running'] ) && (bool) $status['running'] ) { + $html = ''; + } else { + $html = ''; + } + + $form_fields['rescan_pdf'] = [ + 'label' => __( 'Scan PDF for text', 'classifai' ), + 'input' => 'html', + 'html' => $html, + 'show_in_edit' => false, + ]; + + return $form_fields; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'Extract visible text from multi-pages PDF documents. Store the result as the attachment description.', 'classifai' ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'provider' => ComputerVision::ID, + ]; + } +} diff --git a/includes/Classifai/Features/RecommendedContent.php b/includes/Classifai/Features/RecommendedContent.php new file mode 100644 index 000000000..35a7e5a01 --- /dev/null +++ b/includes/Classifai/Features/RecommendedContent.php @@ -0,0 +1,74 @@ +label = __( 'Recommended Content', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( PersonalizerService::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + PersonalizerProvider::ID => __( 'Microsoft AI Personalizer', 'classifai' ), + ]; + } + + /** + * Get the description for the enable field. + * + * @return string + */ + public function get_enable_description(): string { + return esc_html__( 'Enables the ability to generate recommended content data for the block.', 'classifai' ); + } + + /** + * Returns the default settings for the feature. + * + * @return array + */ + public function get_feature_default_settings(): array { + return [ + 'provider' => PersonalizerProvider::ID, + ]; + } + + /** + * Runs the feature. + * + * @param mixed ...$args Arguments required by the feature depending on the provider selected. + * @return mixed + */ + public function run( ...$args ) { + $settings = $this->get_settings(); + $provider_id = $settings['provider'] ?? PersonalizerProvider::ID; + $provider_instance = $this->get_feature_provider_instance( $provider_id ); + $result = ''; + + if ( PersonalizerProvider::ID === $provider_instance::ID ) { + /** @var PersonalizerProvider $provider_instance */ + $result = call_user_func_array( + [ $provider_instance, 'personalizer_send_reward' ], + [ ...$args ] + ); + } + } +} diff --git a/includes/Classifai/Providers/Azure/TextToSpeech.php b/includes/Classifai/Features/TextToSpeech.php similarity index 51% rename from includes/Classifai/Providers/Azure/TextToSpeech.php rename to includes/Classifai/Features/TextToSpeech.php index 7b962e016..c8a81612b 100644 --- a/includes/Classifai/Providers/Azure/TextToSpeech.php +++ b/includes/Classifai/Features/TextToSpeech.php @@ -1,91 +1,90 @@ label = __( 'Text to Speech', 'classifai' ); + + // Contains all providers that are registered to the service. + $this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() ); + + // Contains just the providers this feature supports. + $this->supported_providers = [ + Speech::ID => __( 'Microsoft Azure AI Speech', 'classifai' ), + ]; + } /** - * Meta key to get/set the audio hash that helps to indicate if there is any need - * for the audio file to be regenerated or not. + * Set up necessary hooks. * - * @var string + * We utilize this so we can register the REST route. */ - const AUDIO_HASH_KEY = '_classifai_post_audio_hash'; + public function setup() { + parent::setup(); + add_action( 'rest_api_init', [ $this, 'register_endpoints' ] ); + } /** - * Azure Text to Speech constructor. - * - * @param string $service The service this class belongs to. + * Set up necessary hooks. */ - public function __construct( $service ) { - parent::__construct( - 'Microsoft Azure', - self::FEATURE_NAME, - 'azure_text_to_speech', - $service - ); + public function feature_setup() { + add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); + add_action( 'rest_api_init', [ $this, 'add_meta_to_rest_api' ] ); - // Features provided by this provider. - $this->features = array( - 'text_to_speech' => __( 'Text to speech', 'classifai' ), - ); + foreach ( $this->get_supported_post_types() as $post_type ) { + add_action( 'rest_insert_' . $post_type, [ $this, 'rest_handle_audio' ], 10, 2 ); + } - // Set the onboarding options. - $this->onboarding_options = array( - 'title' => __( 'Microsoft Azure Text to Speech', 'classifai' ), - 'fields' => array( 'url', 'api-key' ), - 'features' => array( - 'enable_text_to_speech' => __( 'Generate speech for post content', 'classifai' ), - ), - ); + add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] ); + add_action( 'save_post', [ $this, 'save_post_metadata' ], 5 ); + + if ( $this->is_enabled() ) { + add_filter( 'the_content', [ $this, 'render_post_audio_controls' ] ); + } } /** @@ -94,15 +93,13 @@ public function __construct( $service ) { * @since 2.4.0 Use get_asset_info to get the asset version and dependencies. */ public function enqueue_editor_assets() { - $post = get_post(); - - if ( empty( $post ) ) { + if ( ! $this->is_feature_enabled() ) { return; } - $supported_post_types = get_tts_supported_post_types(); + $post = get_post(); - if ( ! in_array( $post->post_type, $supported_post_types, true ) ) { + if ( empty( $post ) ) { return; } @@ -125,442 +122,14 @@ public function enqueue_editor_assets() { } /** - * Register the actions needed. - */ - public function register() { - if ( $this->is_feature_enabled( 'text_to_speech' ) ) { - add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] ); - add_action( 'rest_api_init', [ $this, 'add_synthesize_speech_meta_to_rest_api' ] ); - add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] ); - add_action( 'save_post', [ $this, 'save_post_metadata' ], 5 ); - - foreach ( get_tts_supported_post_types() as $post_type ) { - add_action( 'rest_insert_' . $post_type, [ $this, 'rest_handle_audio' ], 10, 2 ); - } - } - - if ( $this->is_enabled( 'text_to_speech' ) ) { - add_filter( 'the_content', [ $this, 'render_post_audio_controls' ] ); - } - } - - /** - * Resets settings for the TextToSpeech provider. - */ - public function reset_settings() { - update_option( $this->get_option_name(), $this->get_default_settings() ); - } - - /** - * Set up the fields for each section. - */ - public function setup_fields_sections() { - add_settings_section( $this->get_option_name(), $this->provider_service_name, '', $this->get_option_name() ); - $default_settings = $this->get_default_settings(); - $voices_options = $this->get_voices_select_options(); - - add_settings_field( - 'url', - esc_html__( 'Endpoint URL', 'classifai' ), - [ $this, 'render_input' ], - $this->get_option_name(), - $this->get_option_name(), - [ - 'option_index' => 'credentials', - 'label_for' => 'url', - 'input_type' => 'text', - 'default_value' => $default_settings['credentials']['url'], - 'description' => __( 'Text to Speech region endpoint, e.g., https://LOCATION.tts.speech.microsoft.com/. Replace LOCATION with the Location/Region you selected for the resource in Azure.', 'classifai' ), - ] - ); - - add_settings_field( - 'api-key', - esc_html__( 'API Key', 'classifai' ), - [ $this, 'render_input' ], - $this->get_option_name(), - $this->get_option_name(), - [ - 'option_index' => 'credentials', - 'label_for' => 'api_key', - 'input_type' => 'password', - 'default_value' => $default_settings['credentials']['api_key'], - ] - ); - - add_settings_field( - 'enable_text_to_speech', - esc_html__( 'Generate speech for post content', 'classifai' ), - [ $this, 'render_input' ], - $this->get_option_name(), - $this->get_option_name(), - [ - 'label_for' => 'enable_text_to_speech', - 'input_type' => 'checkbox', - 'default_value' => $default_settings['enable_text_to_speech'], - 'description' => __( 'Enables speech generation for post content.', 'classifai' ), - ] - ); - - // Add user/role based access settings. - $this->add_access_settings( 'text_to_speech' ); - - add_settings_field( - 'post-types', - esc_html__( 'Post Types', 'classifai' ), - [ $this, 'render_checkbox_group' ], - $this->get_option_name(), - $this->get_option_name(), - [ - 'label_for' => 'post_types', - 'option_index' => 'post_types', - 'options' => $this->get_post_types_select_options(), - 'default_values' => $default_settings['post_types'], - ] - ); - - if ( ! empty( $voices_options ) ) { - add_settings_field( - 'voice', - esc_html__( 'Voice', 'classifai' ), - [ $this, 'render_select' ], - $this->get_option_name(), - $this->get_option_name(), - [ - 'label_for' => 'voice', - 'options' => $voices_options, - 'default_value' => $default_settings['voice'], - ] - ); - } - } - - /** - * Sanitization callback for settings. - * - * @param array $settings The settings being saved. - * @return array - */ - public function sanitize_settings( $settings ) { - $current_settings = wp_parse_args( $this->get_settings(), $this->get_default_settings() ); - $current_settings = array_merge( $current_settings, $this->sanitize_access_settings( $settings, 'text_to_speech' ) ); - $is_credentials_changed = false; - - if ( empty( $settings['enable_text_to_speech'] ) || 1 !== (int) $settings['enable_text_to_speech'] ) { - $current_settings['enable_text_to_speech'] = 'no'; - } else { - $current_settings['enable_text_to_speech'] = '1'; - } - - if ( ! empty( $settings['credentials']['url'] ) && ! empty( $settings['credentials']['api_key'] ) ) { - $new_url = trailingslashit( esc_url_raw( $settings['credentials']['url'] ) ); - $new_key = sanitize_text_field( $settings['credentials']['api_key'] ); - - if ( $new_url !== $current_settings['credentials']['url'] || $new_key !== $current_settings['credentials']['api_key'] ) { - $is_credentials_changed = true; - } - - if ( $is_credentials_changed ) { - $current_settings['credentials']['url'] = $new_url; - $current_settings['credentials']['api_key'] = $new_key; - $current_settings['voices'] = $this->connect_to_service( - array( - 'url' => $new_url, - 'api_key' => $new_key, - ) - ); - - if ( ! empty( $current_settings['voices'] ) ) { - $current_settings['authenticated'] = true; - } else { - $current_settings['voices'] = []; - $current_settings['authenticated'] = false; - } - } - } else { - $current_settings['credentials']['url'] = ''; - $current_settings['credentials']['api_key'] = ''; - - add_settings_error( - $this->get_option_name(), - 'classifai-azure-text-to-speech-auth-empty', - esc_html__( 'One or more credentials required to connect to the Azure Text to Speech service is empty.', 'classifai' ), - 'error' - ); - } - - // Sanitize the post type checkboxes - $post_types = get_post_types_for_language_settings(); - - foreach ( $post_types as $post_type ) { - if ( isset( $settings['post_types'][ $post_type->name ] ) ) { - $current_settings['post_types'][ $post_type->name ] = $settings['post_types'][ $post_type->name ]; - } else { - $current_settings['post_types'][ $post_type->name ] = null; - } - } - - if ( isset( $settings['voice'] ) && ! empty( $settings['voice'] ) ) { - $current_settings['voice'] = sanitize_text_field( $settings['voice'] ); - } - - return $current_settings; - } - - /** - * Connects to Azure's Text to Speech service. - * - * @param array $args Overridable args. - * @return array - */ - public function connect_to_service( array $args = array() ) { - $credentials = $this->get_settings( 'credentials' ); - - $default = array( - 'url' => isset( $credentials['url'] ) ? $credentials['url'] : '', - 'api_key' => isset( $credentials['api_key'] ) ? $credentials['api_key'] : '', - ); - - $default = wp_parse_args( $args, $default ); - - // Return if credentials don't exist. - if ( empty( $default['url'] ) || empty( $default['api_key'] ) ) { - return array(); - } - - // Create request arguments. - $request_params = array( - 'headers' => array( - 'Ocp-Apim-Subscription-Key' => $default['api_key'], - 'Content-Type' => 'application/json', - ), - ); - - // Create request URL. - $request_url = sprintf( - '%1$scognitiveservices/voices/list', - $default['url'] - ); - - if ( function_exists( 'vip_safe_wp_remote_get' ) ) { - $response = vip_safe_wp_remote_get( - $request_url, - '', - 3, - 1, - 20, - $request_params - ); - } else { - $request_params['timeout'] = 20; // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get -- use of `vip_safe_wp_remote_get` is done when available. - $response = wp_remote_get( - $request_url, - $request_params - ); - } - - if ( is_wp_error( $response ) ) { - add_settings_error( - $this->get_option_name(), - 'azure-text-to-request-failed', - esc_html__( 'Azure Speech to Text: HTTP request failed.', 'classifai' ), - 'error' - ); - - return array(); - } - - $http_code = wp_remote_retrieve_response_code( $response ); - - // Return and render error if HTTP response status code is other than 200. - if ( WP_Http::OK !== $http_code ) { - add_settings_error( - $this->get_option_name(), - 'azure-text-to-speech-auth-failed', - esc_html__( 'Connection to Azure Text to Speech failed.', 'classifai' ), - 'error' - ); - - return array(); - } - - $response_body = wp_remote_retrieve_body( $response ); - $voices = json_decode( $response_body ); - $sanitized_voices = array(); - - if ( is_array( $voices ) ) { - foreach ( $voices as $voice ) { - $voice_object = new stdClass(); - - foreach ( $voice as $key => $value ) { - $voice_object->$key = sanitize_text_field( $value ); - } - - $sanitized_voices[] = $voice_object; - } - } - - return $sanitized_voices; - } - - /** - * Returns HTML select dropdown options for voices. - * - * @return array - */ - public function get_voices_select_options() { - $voices = $this->get_settings( 'voices' ); - $options = array(); - - if ( false === $voices ) { - return $options; - } - - foreach ( $voices as $voice ) { - if ( ! is_object( $voice ) ) { - continue; - } - - // phpcs is disabled because it throws error for camel case. - // phpcs:disable - $options[ "{$voice->ShortName}|{$voice->Gender}" ] = sprintf( - '%1$s (%2$s/%3$s)', - esc_html( $voice->LocaleName ), - esc_html( $voice->DisplayName ), - esc_html( $voice->Gender ) - ); - // phpcs:enable - } - - return $options; - } - - /** - * Provides debug information related to the provider. - * - * @param null|array $settings Settings array. If empty, settings will be retrieved. - * @param boolean $configured Whether the provider is correctly configured. If null, the option will be retrieved. - * @return array Keyed array of debug information. - */ - public function get_provider_debug_information( $settings = null, $configured = null ) { - if ( is_null( $settings ) ) { - $settings = $this->sanitize_settings( $this->get_settings() ); - } - - $authenticated = 1 === intval( $settings['authenticated'] ?? 0 ); - - return [ - __( 'Authenticated', 'classifai' ) => $authenticated ? __( 'Yes', 'classifai' ) : __( 'No', 'classifai' ), - __( 'API URL', 'classifai' ) => $settings['url'] ?? '', - __( 'Latest response - Voices', 'classifai' ) => $this->get_formatted_latest_response( $this->get_settings( 'voices' ) ), - ]; - } - - /** - * Returns the default settings. - */ - public function get_default_settings() { - $default_settings = parent::get_default_settings() ?? []; - - return array_merge( - $default_settings, - [ - 'enable_text_to_speech' => false, - 'credentials' => array( - 'url' => '', - 'api_key' => '', - ), - 'voices' => array(), - 'voice' => '', - 'authenticated' => false, - 'post_types' => array( - 'post' => 'post', - ), - ] - ); - } - - /** - * Get the settings and allow for settings default values. - * - * @param string|bool|mixed $index Optional. Name of the settings option index. - * - * @return string|array|mixed + * Add audio related fields to rest API for view/edit. */ - public function get_settings( $index = false ) { - $defaults = $this->get_default_settings(); - $settings = get_option( $this->get_option_name(), [] ); - - // Backward compatibility for enable feature setting. - if ( ! empty( $settings ) && ! isset( $settings['enable_text_to_speech'] ) ) { - $settings['enable_text_to_speech'] = $settings['authenticated'] ?? $defaults['enable_text_to_speech']; - } - - $settings = wp_parse_args( $settings, $defaults ); - - if ( $index && isset( $settings[ $index ] ) ) { - return $settings[ $index ]; + public function add_meta_to_rest_api() { + if ( ! $this->is_feature_enabled() ) { + return; } - return $settings; - } - - /** - * Initial audio generation state. - * - * Fetch the initial state of audio generation prior to the audio existing for the post. - * - * @param int|WP_Post|null $post Optional. Post ID or post object. `null`, `false`, `0` and other PHP falsey values - * return the current global post inside the loop. A numerically valid post ID that - * points to a non-existent post returns `null`. Defaults to global $post. - * @return bool The initial state of audio generation. Default true. - */ - public function get_audio_generation_initial_state( $post = null ) { - /** - * Initial state of the audio generation toggle when no audio already exists for the post. - * - * @since 2.3.0 - * @hook classifai_audio_generation_initial_state - * - * @param {bool} $state Initial state of audio generation toggle on a post. Default true. - * @param {WP_Post} $post The current Post object. - * - * @return {bool} Initial state the audio generation toggle should be set to when no audio exists. - */ - return apply_filters( 'classifai_audio_generation_initial_state', true, get_post( $post ) ); - } - - /** - * Subsequent audio generation state. - * - * Fetch the subsequent state of audio generation once audio is generated for the post. - * - * @param int|WP_Post|null $post Optional. Post ID or post object. `null`, `false`, `0` and other PHP falsey values - * return the current global post inside the loop. A numerically valid post ID that - * points to a non-existent post returns `null`. Defaults to global $post. - * @return bool The subsequent state of audio generation. Default false. - */ - public function get_audio_generation_subsequent_state( $post = null ) { - /** - * Subsequent state of the audio generation toggle when audio exists for the post. - * - * @since 2.3.0 - * @hook classifai_audio_generation_subsequent_state - * - * @param {bool} $state Subsequent state of audio generation toggle on a post. Default false. - * @param {WP_Post} $post The current Post object. - * - * @return {bool} Subsequent state the audio generation toggle should be set to when audio exists. - */ - return apply_filters( 'classifai_audio_generation_subsequent_state', false, get_post( $post ) ); - } - - /** - * Add audio related fields to rest API for view/edit. - */ - public function add_synthesize_speech_meta_to_rest_api() { - $supported_post_types = get_tts_supported_post_types(); + $supported_post_types = $this->get_supported_post_types(); register_rest_field( $supported_post_types, @@ -626,12 +195,15 @@ public function add_synthesize_speech_meta_to_rest_api() { } /** - * Handles audio generation on rest updates / inserts. + * Handles audio generation on REST updates / inserts. * - * @param WP_Post $post Inserted or updated post object. + * @param \WP_Post $post Inserted or updated post object. * @param WP_REST_Request $request Request object. */ - public function rest_handle_audio( $post, $request ) { + public function rest_handle_audio( \WP_Post $post, WP_REST_Request $request ) { + if ( ! $this->is_feature_enabled() ) { + return; + } $audio_id = get_post_meta( $request->get_param( 'id' ), self::AUDIO_ID_KEY, true ); @@ -644,23 +216,125 @@ public function rest_handle_audio( $post, $request ) { $process_content = true; } - // Add/Update audio if it was requested. + // Add/update audio if it was requested. if ( ( $process_content && null === $request->get_param( 'classifai_synthesize_speech' ) ) || true === $request->get_param( 'classifai_synthesize_speech' ) ) { - $save_post_handler = new SavePostHandler(); - $save_post_handler->synthesize_speech( $request->get_param( 'id' ) ); + $results = $this->run( $request->get_param( 'id' ), 'synthesize' ); + + if ( $results && ! is_wp_error( $results ) ) { + $this->save( $results, $request->get_param( 'id' ) ); + } + } + } + + /** + * Register any needed endpoints. + */ + public function register_endpoints() { + register_rest_route( + 'classifai/v1', + 'synthesize-speech/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'rest_endpoint_callback' ), + 'args' => array( + 'id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'description' => esc_html__( 'ID of post to run text to speech conversion on.', 'classifai' ), + ), + ), + 'permission_callback' => [ $this, 'speech_synthesis_permissions_check' ], + ] + ); + } + + /** + * Check if a given request has access to generate audio for the post. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function speech_synthesis_permissions_check( WP_REST_Request $request ) { + $post_id = $request->get_param( 'id' ); + + // Ensure we have a logged in user that can edit the item. + if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { + return false; + } + + $post_type = get_post_type( $post_id ); + $post_type_obj = get_post_type_object( $post_type ); + + // Ensure the post type is allowed in REST endpoints. + if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) { + return false; + } + + // Ensure the post type is supported by this feature. + $supported = $this->get_supported_post_types(); + if ( ! in_array( $post_type, $supported, true ) ) { + return new WP_Error( 'not_enabled', esc_html__( 'Speech synthesis is not enabled for current item.', 'classifai' ) ); + } + + // Ensure the feature is enabled. Also runs a user check. + if ( ! $this->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Speech synthesis is not currently enabled.', 'classifai' ) ); } + + return true; } /** - * Add meta box to post types that support speech synthesis. + * Generic request handler for all our custom routes. * - * @param string $post_type Post type. + * @param WP_REST_Request $request The full request object. + * @return \WP_REST_Response */ - public function add_meta_box( $post_type ) { - if ( ! in_array( $post_type, get_tts_supported_post_types(), true ) ) { + public function rest_endpoint_callback( WP_REST_Request $request ) { + $route = $request->get_route(); + + if ( strpos( $route, '/classifai/v1/synthesize-speech' ) === 0 ) { + $results = $this->run( $request->get_param( 'id' ), 'synthesize' ); + + if ( $results && ! is_wp_error( $results ) ) { + $attachment_id = $this->save( $results, $request->get_param( 'id' ) ); + + if ( ! is_wp_error( $attachment_id ) ) { + return rest_ensure_response( + array( + 'success' => true, + 'audio_id' => $attachment_id, + ) + ); + } + } + + return rest_ensure_response( + array( + 'success' => false, + 'code' => $results->get_error_code(), + 'message' => $results->get_error_message(), + ) + ); + } + + return parent::rest_endpoint_callback( $request ); + } + + /** + * Adds a meta box for Classic content to trigger Text to Speech. + * + * @param string $post_type The post type. + */ + public function add_meta_box( string $post_type ) { + if ( + ! in_array( $post_type, $this->get_supported_post_types(), true ) || + ! $this->is_feature_enabled() + ) { return; } @@ -680,11 +354,12 @@ public function add_meta_box( $post_type ) { * * @param \WP_Post $post WP_Post object. */ - public function render_meta_box( $post ) { + public function render_meta_box( \WP_Post $post ) { wp_nonce_field( 'classifai_text_to_speech_meta_action', 'classifai_text_to_speech_meta' ); $source_url = false; $audio_id = get_post_meta( $post->ID, self::AUDIO_ID_KEY, true ); + if ( $audio_id ) { $source_url = wp_get_attachment_url( $audio_id ); } @@ -708,8 +383,8 @@ public function render_meta_box( $post ) { if ( $post_type ) { $post_type_label = $post_type->labels->singular_name; } - ?> +

-
-
- $feature ) : - ?> -
-
-
- -
- -
- +
@@ -238,50 +197,30 @@ public function render_settings_page() { /** * Adds plugin debug information to be printed on the Site Health screen. * + * @since 1.4.0 + * * @param array $debug_information Array of associative arrays corresponding to lines of debug information. * @return array Array with lines added. - * @since 1.4.0 */ - public function add_service_debug_information( $debug_information ) { + public function add_service_debug_information( array $debug_information ): array { return array_merge( $debug_information, $this->get_service_debug_information() ); } /** * Provides debug information for the service. * - * @return array Array of associative arrays representing lines of debug information. * @since 1.4.0 + * + * @return array Array of associative arrays representing lines of debug information. */ - public function get_service_debug_information() { - $make_line = function ( $provider ) { + public function get_service_debug_information(): array { + $make_line = function ( $feature ) { return [ - 'label' => sprintf( '%s: %s', $this->get_display_name(), $provider->get_provider_name() ), - 'value' => $provider->get_provider_debug_information(), + 'label' => sprintf( '%s', $feature->get_label() ), + 'value' => $feature->get_debug_information(), ]; }; - return array_map( $make_line, $this->provider_classes ); - } - - /** - * Check if the current user has permission to create and assign terms. - * - * @param string $tax Taxonomy name. - * @return bool|WP_Error - */ - public function check_term_permissions( string $tax = '' ) { - $taxonomy = get_taxonomy( $tax ); - - if ( empty( $taxonomy ) || empty( $taxonomy->show_in_rest ) ) { - return new WP_Error( 'invalid_taxonomy', esc_html__( 'Taxonomy not found. Double check your settings.', 'classifai' ) ); - } - - $create_cap = is_taxonomy_hierarchical( $taxonomy->name ) ? $taxonomy->cap->edit_terms : $taxonomy->cap->assign_terms; - - if ( ! current_user_can( $create_cap ) || ! current_user_can( $taxonomy->cap->assign_terms ) ) { - return new WP_Error( 'rest_cannot_assign_term', esc_html__( 'Sorry, you are not alllowed to create or assign to this taxonomy.', 'classifai' ) ); - } - - return true; + return array_map( $make_line, $this->feature_classes ); } } diff --git a/includes/Classifai/Services/ServicesManager.php b/includes/Classifai/Services/ServicesManager.php index 033e81150..29092fe7f 100644 --- a/includes/Classifai/Services/ServicesManager.php +++ b/includes/Classifai/Services/ServicesManager.php @@ -1,6 +1,6 @@ services = $services; $this->service_classes = []; $this->get_menu_title(); @@ -42,6 +42,10 @@ public function __construct( $services = [] ) { * Register the actions required for the settings page. */ public function register() { + add_filter( 'language_processing_features', [ $this, 'register_language_processing_features' ] ); + add_filter( 'image_processing_features', [ $this, 'register_image_processing_features' ] ); + add_filter( 'personalizer_features', [ $this, 'register_recommendation_service_features' ] ); + foreach ( $this->services as $key => $service ) { if ( class_exists( $service ) ) { $this->service_classes[ $key ] = new $service(); @@ -57,10 +61,48 @@ public function register() { add_filter( 'classifai_debug_information', [ $this, 'add_debug_information' ], 1 ); } + /** + * Registers features under the Language Processing Service. + */ + public function register_language_processing_features(): array { + return [ + '\Classifai\Features\Classification', + '\Classifai\Features\TitleGeneration', + '\Classifai\Features\ExcerptGeneration', + '\Classifai\Features\ContentResizing', + '\Classifai\Features\TextToSpeech', + '\Classifai\Features\AudioTranscriptsGeneration', + ]; + } + + /** + * Registers features under the Image Processing Service. + */ + public function register_image_processing_features(): array { + return [ + '\Classifai\Features\DescriptiveTextGenerator', + '\Classifai\Features\ImageTagsGenerator', + '\Classifai\Features\ImageCropping', + '\Classifai\Features\ImageTextExtraction', + '\Classifai\Features\ImageGeneration', + '\Classifai\Features\PDFTextExtraction', + ]; + } + + /** + * Registers features under the Recommendation Service. + */ + public function register_recommendation_service_features(): array { + return [ + '\Classifai\Features\RecommendedContent', + ]; + } + /** * Get general ClassifAI settings * * @param string $index Optional specific setting to be retrieved. + * @return mixed */ public function get_settings( $index = false ) { $settings = get_option( 'classifai_settings' ); @@ -83,11 +125,8 @@ public function get_settings( $index = false ) { } } - /** * Create the settings pages. - * - * If there are more than a single service, we'll create a top level admin menu and add subsequent items there. */ public function do_settings() { add_action( 'admin_menu', [ $this, 'register_admin_menu_item' ] ); @@ -97,8 +136,6 @@ public function do_settings() { /** * Register the settings and sanitization callback method. - * - * It's very important that the option group matches the page slug. */ public function register_settings() { register_setting( 'classifai_settings', 'classifai_settings', [ $this, 'sanitize_settings' ] ); @@ -107,15 +144,16 @@ public function register_settings() { /** * Sanitize settings. * - * @param array $settings The settings to be sanitized. - * - * @return mixed + * @param mixed $settings The settings to be sanitized. + * @return array */ - public function sanitize_settings( $settings ) { + public function sanitize_settings( $settings ): array { $new_settings = []; + if ( isset( $settings['email'] ) && isset( $settings['license_key'] ) - && $this->check_license_key( $settings['email'], $settings['license_key'] ) ) { + && $this->check_license_key( $settings['email'], $settings['license_key'] ) + ) { $new_settings['valid_license'] = true; $new_settings['email'] = sanitize_text_field( $settings['email'] ); $new_settings['license_key'] = sanitize_text_field( $settings['license_key'] ); @@ -123,6 +161,7 @@ public function sanitize_settings( $settings ) { $new_settings['valid_license'] = false; $new_settings['email'] = isset( $settings['email'] ) ? sanitize_text_field( $settings['email'] ) : ''; $new_settings['license_key'] = isset( $settings['license_key'] ) ? sanitize_text_field( $settings['license_key'] ) : ''; + add_settings_error( 'registration', 'classifai-registration', @@ -198,7 +237,6 @@ protected function register_services() { } } - /** * Helper to return the $menu title */ @@ -220,7 +258,7 @@ protected function get_menu_title() { * * @return array */ - public function get_services() { + public function get_services(): array { return $this->services; } @@ -270,7 +308,7 @@ public function render_settings_page() { service_classes[0]->render_settings_page(); } } @@ -278,12 +316,13 @@ public function render_settings_page() { /** * Hit license API to see if key/email is valid * - * @param string $email Email address. - * @param string $license_key License key. * @since 1.2 + * + * @param string $email Email address. + * @param string $license_key License key. * @return bool */ - public function check_license_key( $email, $license_key ) { + public function check_license_key( string $email, string $license_key ): bool { $request = wp_remote_post( 'https://classifaiplugin.com/wp-json/classifai-theme/v1/validate-license', @@ -310,12 +349,13 @@ public function check_license_key( $email, $license_key ) { /** * Adds debug information to the ClassifAI Site Health screen. * + * @since 1.4.0 + * * @param array $debug_information Array of lines representing debug information. * @param array|null $settings Settings array. If empty, will be fetched. * @return array Array with lines added. - * @since 1.4.0 */ - public function add_debug_information( $debug_information, $settings = null ) { + public function add_debug_information( array $debug_information, $settings = null ): array { if ( is_null( $settings ) ) { $settings = $this->sanitize_settings( $this->get_settings() ); } diff --git a/includes/Classifai/Taxonomy/AbstractTaxonomy.php b/includes/Classifai/Taxonomy/AbstractTaxonomy.php index a44dcfe34..4395c40b0 100644 --- a/includes/Classifai/Taxonomy/AbstractTaxonomy.php +++ b/includes/Classifai/Taxonomy/AbstractTaxonomy.php @@ -33,21 +33,21 @@ abstract class AbstractTaxonomy { * * @return string */ - abstract public function get_name(); + abstract public function get_name(): string; /** * Get the singular taxonomy label. * * @return string */ - abstract public function get_singular_label(); + abstract public function get_singular_label(): string; /** * Get the plural taxonomy label. * * @return string */ - abstract public function get_plural_label(); + abstract public function get_plural_label(): string; /** * Return true or false based on whether to show this taxonomy. Maps @@ -55,7 +55,7 @@ abstract public function get_plural_label(); * * @return bool */ - abstract public function get_visibility(); + abstract public function get_visibility(): bool; /** * Register hooks and actions. @@ -73,7 +73,7 @@ public function register() { * * @return array */ - public function get_options() { + public function get_options(): array { $visibility = $this->get_visibility(); return array( @@ -93,18 +93,16 @@ public function get_options() { * * @return string */ - public function update_count_callback() { + public function update_count_callback(): string { return ''; } - - /** * Get the labels for the taxonomy. * * @return array */ - public function get_labels() { + public function get_labels(): array { $plural_label = $this->get_plural_label(); $singular_label = $this->get_singular_label(); diff --git a/includes/Classifai/Taxonomy/CategoryTaxonomy.php b/includes/Classifai/Taxonomy/CategoryTaxonomy.php index 4c562213c..e805ad882 100644 --- a/includes/Classifai/Taxonomy/CategoryTaxonomy.php +++ b/includes/Classifai/Taxonomy/CategoryTaxonomy.php @@ -2,6 +2,9 @@ namespace Classifai\Taxonomy; +use function Classifai\Providers\Watson\get_feature_enabled; +use function Classifai\Providers\Watson\get_feature_taxonomy; + /** * The Classifai Category Taxonomy. * @@ -18,30 +21,38 @@ class CategoryTaxonomy extends AbstractTaxonomy { /** * Get the ClassifAI category taxonomy name. + * + * @return string */ - public function get_name() { + public function get_name(): string { return WATSON_CATEGORY_TAXONOMY; } /** * Get the ClassifAI category taxonomy label. + * + * @return string */ - public function get_singular_label() { + public function get_singular_label(): string { return esc_html__( 'Watson Category', 'classifai' ); } /** * Get the ClassifAI category taxonomy plural label. + * + * @return string */ - public function get_plural_label() { + public function get_plural_label(): string { return esc_html__( 'Watson Categories', 'classifai' ); } /** * Get the ClassifAI category taxonomy visibility. + * + * @return bool */ - public function get_visibility() { - return \Classifai\get_feature_enabled( 'category' ) && - \Classifai\get_feature_taxonomy( 'category' ) === $this->get_name(); + public function get_visibility(): bool { + return get_feature_enabled( 'category' ) && + get_feature_taxonomy( 'category' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/ConceptTaxonomy.php b/includes/Classifai/Taxonomy/ConceptTaxonomy.php index 3363e548b..fe6d4003a 100644 --- a/includes/Classifai/Taxonomy/ConceptTaxonomy.php +++ b/includes/Classifai/Taxonomy/ConceptTaxonomy.php @@ -2,6 +2,9 @@ namespace Classifai\Taxonomy; +use function Classifai\Providers\Watson\get_feature_enabled; +use function Classifai\Providers\Watson\get_feature_taxonomy; + /** * The Classifai Concept Taxonomy. * @@ -18,30 +21,38 @@ class ConceptTaxonomy extends AbstractTaxonomy { /** * Get the ClassifAI concept taxonomy name. + * + * @return string */ - public function get_name() { + public function get_name(): string { return WATSON_CONCEPT_TAXONOMY; } /** * Get the ClassifAI concept taxonomy label. + * + * @return string */ - public function get_singular_label() { + public function get_singular_label(): string { return esc_html__( 'Watson Concept', 'classifai' ); } /** * Get the ClassifAI concept taxonomy plural label. + * + * @return string */ - public function get_plural_label() { + public function get_plural_label(): string { return esc_html__( 'Watson Concepts', 'classifai' ); } /** * Get the ClassifAI concept taxonomy visibility. + * + * @return bool */ - public function get_visibility() { - return \Classifai\get_feature_enabled( 'concept' ) && - \Classifai\get_feature_taxonomy( 'concept' ) === $this->get_name(); + public function get_visibility(): bool { + return get_feature_enabled( 'concept' ) && + get_feature_taxonomy( 'concept' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/EntityTaxonomy.php b/includes/Classifai/Taxonomy/EntityTaxonomy.php index e91f0e419..526924fd1 100644 --- a/includes/Classifai/Taxonomy/EntityTaxonomy.php +++ b/includes/Classifai/Taxonomy/EntityTaxonomy.php @@ -2,6 +2,9 @@ namespace Classifai\Taxonomy; +use function Classifai\Providers\Watson\get_feature_enabled; +use function Classifai\Providers\Watson\get_feature_taxonomy; + /** * The ClassifAI Entity Taxonomy. * @@ -18,30 +21,38 @@ class EntityTaxonomy extends AbstractTaxonomy { /** * Get the ClassifAI entity taxonomy name. + * + * @return string */ - public function get_name() { + public function get_name(): string { return WATSON_ENTITY_TAXONOMY; } /** * Get the ClassifAI entity taxonomy label. + * + * @return string */ - public function get_singular_label() { + public function get_singular_label(): string { return esc_html__( 'Watson Entity', 'classifai' ); } /** * Get the ClassifAI entity taxonomy plural label. + * + * @return string */ - public function get_plural_label() { + public function get_plural_label(): string { return esc_html__( 'Watson Entities', 'classifai' ); } /** * Get the ClassifAI entity taxonomy visibility. + * + * @return bool */ - public function get_visibility() { - return \Classifai\get_feature_enabled( 'entity' ) && - \Classifai\get_feature_taxonomy( 'entity' ) === $this->get_name(); + public function get_visibility(): bool { + return get_feature_enabled( 'entity' ) && + get_feature_taxonomy( 'entity' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/ImageTagTaxonomy.php b/includes/Classifai/Taxonomy/ImageTagTaxonomy.php index 99e32c2a8..b48b8cf60 100644 --- a/includes/Classifai/Taxonomy/ImageTagTaxonomy.php +++ b/includes/Classifai/Taxonomy/ImageTagTaxonomy.php @@ -6,29 +6,37 @@ class ImageTagTaxonomy extends AbstractTaxonomy { /** * Get the ClassifAI category taxonomy name. + * + * @return string */ - public function get_name() { + public function get_name(): string { return 'classifai-image-tags'; } /** * Get the ClassifAI category taxonomy label. + * + * @return string */ - public function get_singular_label() { + public function get_singular_label(): string { return esc_html__( 'Image Tag', 'classifai' ); } /** * Get the ClassifAI category taxonomy plural label. + * + * @return string */ - public function get_plural_label() { + public function get_plural_label(): string { return esc_html__( 'Image Tags', 'classifai' ); } /** * Get the ClassifAI category taxonomy visibility. + * + * @return bool */ - public function get_visibility() { + public function get_visibility(): bool { return true; } @@ -37,7 +45,7 @@ public function get_visibility() { * * @return string */ - public function update_count_callback() { + public function update_count_callback(): string { return '_update_generic_term_count'; } } diff --git a/includes/Classifai/Taxonomy/KeywordTaxonomy.php b/includes/Classifai/Taxonomy/KeywordTaxonomy.php index 9c0fed4b2..29cc272b6 100644 --- a/includes/Classifai/Taxonomy/KeywordTaxonomy.php +++ b/includes/Classifai/Taxonomy/KeywordTaxonomy.php @@ -2,6 +2,9 @@ namespace Classifai\Taxonomy; +use function Classifai\Providers\Watson\get_feature_enabled; +use function Classifai\Providers\Watson\get_feature_taxonomy; + /** * The ClassifAI Keyword Taxonomy. * @@ -18,30 +21,38 @@ class KeywordTaxonomy extends AbstractTaxonomy { /** * Get the ClassifAI keyword taxonomy name. + * + * @return string */ - public function get_name() { + public function get_name(): string { return WATSON_KEYWORD_TAXONOMY; } /** * Get the ClassifAI keyword taxonomy label. + * + * @return string */ - public function get_singular_label() { + public function get_singular_label(): string { return esc_html__( 'Watson Keyword', 'classifai' ); } /** * Get the ClassifAI keyword taxonomy plural label. + * + * @return string */ - public function get_plural_label() { + public function get_plural_label(): string { return esc_html__( 'Watson Keywords', 'classifai' ); } /** * Get the ClassifAI keyword taxonomy visibility. + * + * @return bool */ - public function get_visibility() { - return \Classifai\get_feature_enabled( 'keyword' ) && - \Classifai\get_feature_taxonomy( 'keyword' ) === $this->get_name(); + public function get_visibility(): bool { + return get_feature_enabled( 'keyword' ) && + get_feature_taxonomy( 'keyword' ) === $this->get_name(); } } diff --git a/includes/Classifai/Taxonomy/TaxonomyFactory.php b/includes/Classifai/Taxonomy/TaxonomyFactory.php index 72cc8ec14..344ca2fda 100644 --- a/includes/Classifai/Taxonomy/TaxonomyFactory.php +++ b/includes/Classifai/Taxonomy/TaxonomyFactory.php @@ -2,6 +2,8 @@ namespace Classifai\Taxonomy; +use function Classifai\Providers\Watson\get_supported_post_types; + /** * TaxonomyFactory builds the Taxonomy taxonomy class instances. Instances * are stored locally and returned from cache on subsequent build calls. @@ -39,11 +41,13 @@ class TaxonomyFactory { public $taxonomies = []; /** - * Builds all supported taxonomies. This is bound to the 'init' hook - * to allow both frontend and backend to get these taxonomies. + * Builds all supported taxonomies. + * + * This is bound to the 'init' hook to allow both + * frontend and backend to get these taxonomies. */ public function build_all() { - $supported_post_types = \Classifai\get_supported_post_types(); + $supported_post_types = get_supported_post_types(); foreach ( $this->get_supported_taxonomies() as $taxonomy ) { $this->build_if( $taxonomy, $supported_post_types ); @@ -55,10 +59,9 @@ public function build_all() { * * @param string $taxonomy The taxonomy name. * @param array $supported_post_types The supported post types. - * * @return BaseTaxonomy A base taxonomy subclass instance. */ - public function build_if( $taxonomy, $supported_post_types = [] ) { + public function build_if( string $taxonomy, array $supported_post_types = [] ) { if ( ! $this->exists( $taxonomy ) ) { $this->taxonomies[ $taxonomy ] = $this->build( $taxonomy ); $instance = $this->taxonomies[ $taxonomy ]; @@ -76,14 +79,14 @@ public function build_if( $taxonomy, $supported_post_types = [] ) { /** * Instantiates and returns a instance for the specified taxonomy. + * * An exception is thrown if an invalid taxonomy name was specified. * * @param string $taxonomy The taxonomy name - * * @return \Taxonomy\Taxonomy\BaseTaxonomy A base taxonomy subclass instance. * @throws \Exception An exception. */ - public function build( $taxonomy ) { + public function build( string $taxonomy ) { if ( ! empty( $this->mapping[ $taxonomy ] ) ) { $class = $this->mapping[ $taxonomy ]; @@ -106,7 +109,7 @@ public function build( $taxonomy ) { * @param string $taxonomy The taxonomy name * @return bool True if the taxonomy exists else false */ - public function exists( $taxonomy ) { + public function exists( string $taxonomy ): bool { return ! empty( $this->taxonomies[ $taxonomy ] ); } @@ -115,7 +118,7 @@ public function exists( $taxonomy ) { * * @return array List of taxonomy names */ - public function get_supported_taxonomies() { + public function get_supported_taxonomies(): array { return array_keys( $this->mapping ); } } diff --git a/package-lock.json b/package-lock.json index 19edbc4c2..7a14297f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,25 +9,25 @@ "version": "2.5.1", "license": "GPL-2.0-or-later", "dependencies": { - "@wordpress/icons": "^9.26.0", + "@wordpress/icons": "^9.41.0", "choices.js": "^10.2.0", "tippy.js": "^6.3.7" }, "devDependencies": { "@10up/cypress-wp-utils": "^0.2.0", - "@wordpress/env": "^8.13.0", - "@wordpress/scripts": "^26.18.0", - "cypress": "^13.6.1", + "@wordpress/env": "^9.2.0", + "@wordpress/scripts": "^27.1.0", + "cypress": "^13.6.4", "cypress-file-upload": "^5.0.8", - "cypress-mochawesome-reporter": "^3.7.0", + "cypress-mochawesome-reporter": "^3.8.1", "cypress-plugin-tab": "^1.0.5", "husky": "^8.0.3", "jsdoc": "^3.6.11", - "lint-staged": "^13.2.2", + "lint-staged": "^15.2.0", "mochawesome-json-to-md": "^0.7.2", "node-wp-i18n": "^1.2.7", "svg-react-loader": "^0.4.6", - "webpack": "^5.86.0", + "webpack": "^5.90.0", "webpack-cli": "^5.1.4", "wp-hookdoc": "^0.2.0" } @@ -187,9 +187,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz", - "integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.9.tgz", + "integrity": "sha512-xPndlO7qxiJbn0ATvfXQBjCS7qApc9xmKHArgI/FTEFxXas5dnjC/VqM37lfZun9dclRYcn+YQAr6uDFy0bB2g==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -336,9 +336,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1722,16 +1722,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.4.tgz", - "integrity": "sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.9.tgz", + "integrity": "sha512-A7clW3a0aSjm3ONU9o2HAILSegJCYlEZmOhmBRReVtIpY/Z/p7yIZ+wR41Z+UipwdGuqwtID/V/dOdZXjwi9gQ==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "semver": "^6.3.1" }, "engines": { @@ -1741,6 +1741,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2298,9 +2311,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2337,9 +2350,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", - "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2361,13 +2374,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2388,9 +2401,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -2845,9 +2858,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -2861,21 +2874,15 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -2941,19 +2948,11 @@ "node": ">= 8" } }, - "node_modules/@pkgr/utils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", - "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "fast-glob": "^3.3.0", - "is-glob": "^4.0.3", - "open": "^9.1.0", - "picocolors": "^1.0.0", - "tslib": "^2.6.0" - }, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -2961,94 +2960,14 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@pkgr/utils/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/utils/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pkgr/utils/node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", - "dev": true, - "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@pkgr/utils/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@pkgr/utils/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@pkgr/utils/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@playwright/test": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", - "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", + "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", "dev": true, "peer": true, "dependencies": { - "playwright": "1.40.1" + "playwright": "1.41.1" }, "bin": { "playwright": "cli.js" @@ -3183,9 +3102,9 @@ } }, "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "dependencies": { "b4a": "^1.6.4", @@ -3818,9 +3737,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", - "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" @@ -3837,9 +3756,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", - "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" @@ -3916,9 +3835,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/express": { @@ -4109,9 +4028,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/qs": { "version": "6.9.10", @@ -4126,9 +4045,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.8.tgz", - "integrity": "sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==", + "version": "18.2.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", + "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4136,9 +4055,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", - "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", "dependencies": { "@types/react": "*" } @@ -4159,9 +4078,9 @@ "dev": true }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/semver": { "version": "7.5.6", @@ -4331,16 +4250,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", - "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", + "integrity": "sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/type-utils": "6.13.1", - "@typescript-eslint/utils": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/type-utils": "6.20.0", + "@typescript-eslint/utils": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4399,15 +4318,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", - "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", + "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4" }, "engines": { @@ -4427,13 +4346,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", - "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", + "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1" + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4444,13 +4363,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", - "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz", + "integrity": "sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/utils": "6.20.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -4471,9 +4390,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", - "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", + "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4484,16 +4403,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", - "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", + "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -4510,6 +4430,15 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4522,6 +4451,21 @@ "node": ">=10" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4544,17 +4488,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", - "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.20.0.tgz", + "integrity": "sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", "semver": "^7.5.4" }, "engines": { @@ -4602,12 +4546,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", - "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", + "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/types": "6.20.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -4827,23 +4771,23 @@ } }, "node_modules/@wordpress/api-fetch": { - "version": "6.44.0", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.44.0.tgz", - "integrity": "sha512-d8ouvBiKDFu67O9Y8MtlUR2YojCAjmLf0LuBKsSOS5r3MOiwte1tQwsLdzFmGYkdCK09mZhT3UVKdOOiAC3kKA==", + "version": "6.47.0", + "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-6.47.0.tgz", + "integrity": "sha512-NA/jWDXoVtJmiVBYhlxts2UrgKJpJM+zTGzLCfRQCZUzpJYm3LonB8x+uCQ78nEyxCY397Esod3jnbquYjOr0Q==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.47.0", - "@wordpress/url": "^3.48.0" + "@wordpress/i18n": "^4.50.0", + "@wordpress/url": "^3.51.0" }, "engines": { "node": ">=12" } }, "node_modules/@wordpress/babel-plugin-import-jsx-pragma": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-4.30.0.tgz", - "integrity": "sha512-UKkyFmEYk1UTO0ZPun6Kw5dNflTEDpDK/6RxAqxbVrsIWUVSkVahwBnqfS0v5LuvVU8y+5vJSR/WjlnKEmS3Sg==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-4.33.0.tgz", + "integrity": "sha512-CjzruFKWgzU/mO/nnQJ2l9UlzZQpqS60UC6l2vNdJ9oD2nKHR5Oou6kNic3QhWDVJrBf2JUiJJ0TC280bykXmA==", "dev": true, "engines": { "node": ">=14" @@ -4853,9 +4797,9 @@ } }, "node_modules/@wordpress/babel-preset-default": { - "version": "7.31.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-7.31.0.tgz", - "integrity": "sha512-LAiTOlolFvKW6xmL6qRkdbPG09LPwAsmDepz4zWrFXJZHSImDeO2QXHecF1GnFyzLLKr1myHR5MbN3K5MSzpqQ==", + "version": "7.34.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-7.34.0.tgz", + "integrity": "sha512-yjFOllyTktFHtcIEgU3ghXBn8lItzr5mPLf0xdSpe0cHceFYL1hT1oprhgRL+olZweaO96Yfm0qUCCKQfJBWsA==", "dev": true, "dependencies": { "@babel/core": "^7.16.0", @@ -4864,9 +4808,9 @@ "@babel/preset-env": "^7.16.0", "@babel/preset-typescript": "^7.16.0", "@babel/runtime": "^7.16.0", - "@wordpress/babel-plugin-import-jsx-pragma": "^4.30.0", - "@wordpress/browserslist-config": "^5.30.0", - "@wordpress/warning": "^2.47.0", + "@wordpress/babel-plugin-import-jsx-pragma": "^4.33.0", + "@wordpress/browserslist-config": "^5.33.0", + "@wordpress/warning": "^2.50.0", "browserslist": "^4.21.10", "core-js": "^3.31.0", "react": "^18.2.0" @@ -4876,45 +4820,44 @@ } }, "node_modules/@wordpress/base-styles": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-4.38.0.tgz", - "integrity": "sha512-w491MMHfoCHdWibyTAcmGWvXwNMptslFQOU+jQ5DVeDIgDux1KLo/7oZ41CCHwqYayrCf60BC9+JopDXqq1H+g==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-4.41.0.tgz", + "integrity": "sha512-MjPAZeAqvyskDXDp2wGZ0DjtYOQLOydI1WqVIZS4wnIdhsQWQD//VMeXgLrcmCzNyQg+iKTx3o+BzmXVTOD0+w==", "dev": true }, "node_modules/@wordpress/browserslist-config": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.30.0.tgz", - "integrity": "sha512-HFgLCkvvxba+j7/qNjVn1od38tvMm1xVlIJBR+zukkTvvLu/AkdelWKAQpvAoFAXMaZJ7239VxDVBYbVolf6FQ==", + "version": "5.33.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.33.0.tgz", + "integrity": "sha512-dv1ZlpqGk8gaSBJPP/Z/1uOuxjtP0EBsHVKInLRu6FWLTJkK8rnCeC3xJT3/2TtJ0rasLC79RoytfhXTOODVwg==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@wordpress/dependency-extraction-webpack-plugin": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-4.30.0.tgz", - "integrity": "sha512-Z3AcceaoHFvJdRNVp8rf6EI+rxK0gUMGMfcXYZPAoaDhP6Gt0bsbVMP5zQH2EYl7JHsbRZIQmMqd2fG5E/VjSQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-5.1.0.tgz", + "integrity": "sha512-W2W+9JNAaGirAtGDSf83pjEKb63DLhgpJGgvMOpEPoRPtucgO6CCm3uMoNkJTpKoxJQ2tSZEymAhF/YdLm+ScQ==", "dev": true, "dependencies": { - "json2php": "^0.0.7", - "webpack-sources": "^3.2.2" + "json2php": "^0.0.7" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "webpack": "^4.8.3 || ^5.0.0" + "webpack": "^5.0.0" } }, "node_modules/@wordpress/e2e-test-utils-playwright": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.15.0.tgz", - "integrity": "sha512-ZqCYcxT0Gc59isS42Q7WTQVu3ace8DDEED/RR8loTG+YjqEB1pW5hALFiVXBtM6vSjnnDO0M1NYAldh8l7SCmA==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-0.18.0.tgz", + "integrity": "sha512-Z8uH1dUzy/STQjOU6eb9nquVK4RC1rUx0gXY/GN1IVNDJvGN/yJxT/gNKmfiL7KpmHvNp2Q5M4bnUT9uiNcM+Q==", "dev": true, "dependencies": { - "@wordpress/api-fetch": "^6.44.0", - "@wordpress/keycodes": "^3.47.0", - "@wordpress/url": "^3.48.0", + "@wordpress/api-fetch": "^6.47.0", + "@wordpress/keycodes": "^3.50.0", + "@wordpress/url": "^3.51.0", "change-case": "^4.1.2", "form-data": "^4.0.0", "get-port": "^5.1.1", @@ -4944,14 +4887,14 @@ } }, "node_modules/@wordpress/element": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.12.0.tgz", - "integrity": "sha512-W2Gcg8G9Qbzvh/9smHgvisoepe+GWzHXdxXOdRclNtmNXv0GGRkJJRIm2JFeV7emc2rOiI68VM/khnSTc293sQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.27.0.tgz", + "integrity": "sha512-IA5LTAfx5bDNXULPmctcNb/04i4JcnIReG0RAuPgrZ8lbMZWUxGFymh10PEQjs7ZJ++qGsI6E+6JISpjkRaDQQ==", "dependencies": { "@babel/runtime": "^7.16.0", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", - "@wordpress/escape-html": "^2.35.0", + "@wordpress/escape-html": "^2.50.0", "change-case": "^4.1.2", "is-plain-object": "^5.0.0", "react": "^18.2.0", @@ -4962,14 +4905,14 @@ } }, "node_modules/@wordpress/env": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-8.13.0.tgz", - "integrity": "sha512-rtrrBO22DnbLsdBlsGqlMQrjz1dZfbwGnxyKev+gFd1rSfmLs+1F8L89RHOx9vsGPixl5uRwoU/qgYo7Hf1NVQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-9.2.0.tgz", + "integrity": "sha512-2gl65WYbkuTjnW2SHKjeqdpLTgnPc/xVvFiwG+2p/RJwDHSuw1xXSdFqFUh3+wC/4cuXy9b2ZBm/SYsBoc8DDw==", "dev": true, "dependencies": { "chalk": "^4.0.0", "copy-dir": "^1.3.0", - "docker-compose": "^0.22.2", + "docker-compose": "^0.24.3", "extract-zip": "^1.6.7", "got": "^11.8.5", "inquirer": "^7.1.0", @@ -4985,9 +4928,9 @@ } }, "node_modules/@wordpress/escape-html": { - "version": "2.35.0", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.35.0.tgz", - "integrity": "sha512-tS/+pHBI3Yqkhy2hQ+dKlxm076ULCVa4hk0bgJFtdu0KejQ9wpC7vh/+i8bkv+OQZJx5B8v86872ccO2dKSciw==", + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.50.0.tgz", + "integrity": "sha512-hBvoMCEZocziZDGCmBanSO+uupnd054mxd7FQ6toQ4UnsZ4JwXSmEC72W2Ed+cRGB1DeJDD0dY9iC0b4xkumsQ==", "dependencies": { "@babel/runtime": "^7.16.0" }, @@ -4996,16 +4939,16 @@ } }, "node_modules/@wordpress/eslint-plugin": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-17.4.0.tgz", - "integrity": "sha512-CT19Ib1Y0ttVQm/bOtjGP6Ge5eqfEaUSobTqCWreHt1RIoxJXTDmazJ1g0Q5MjqG4dEZ/Q/FI4sdqyiKRySkbQ==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-17.7.0.tgz", + "integrity": "sha512-JSFaCogE0WlZpl0SV4q8DK8G6jwDjEzXRzOsgesWilea4OuVp1KxCamkddTorRNM3QAbjrGuPJ4NYaGrNG9QsA==", "dev": true, "dependencies": { "@babel/eslint-parser": "^7.16.0", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", - "@wordpress/babel-preset-default": "^7.31.0", - "@wordpress/prettier-config": "^3.4.0", + "@wordpress/babel-preset-default": "^7.34.0", + "@wordpress/prettier-config": "^3.7.0", "cosmiconfig": "^7.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "^2.25.2", @@ -5039,9 +4982,9 @@ } }, "node_modules/@wordpress/eslint-plugin/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5066,9 +5009,9 @@ } }, "node_modules/@wordpress/hooks": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.47.0.tgz", - "integrity": "sha512-a0mZ+lSUBrmacJGXDnFTaz1O47sQgTCZi3LrY445WNc7cmiSlscTfeBxrUXaTF0ninzHJnE7evCIeKLbQC3dLQ==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.50.0.tgz", + "integrity": "sha512-YIhwT1y0ss7Byfz46NBx08EUmXzWMu+g5DCY7FMuDNhwxSEoZMB8edKMiwNmFk4mFKBCnXM1d5FeONUPIUkJwg==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0" @@ -5078,13 +5021,13 @@ } }, "node_modules/@wordpress/i18n": { - "version": "4.47.0", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.47.0.tgz", - "integrity": "sha512-7qOeSChhI8drcnKAbpM2yP2HSWRR0U8xvww3Febd3kGhMKAUp8AMpjyC4rWucak4+Eg1HFfahurCmBt3FxgbYQ==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.50.0.tgz", + "integrity": "sha512-FkA2se6HMQm4eFC+/kTWvWQqs51VxpZuvY2MlWUp/L1r1d/dMBHXu049x86+/+6yk3ZNqiK5h6j6Z76dvPHZ4w==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/hooks": "^3.47.0", + "@wordpress/hooks": "^3.50.0", "gettext-parser": "^1.3.1", "memize": "^2.1.0", "sprintf-js": "^1.1.1", @@ -5114,22 +5057,22 @@ "dev": true }, "node_modules/@wordpress/icons": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-9.26.0.tgz", - "integrity": "sha512-5tS2DqFG++544Sopiz7z5cmNIgtJUxBrnwcElUvyGT8+eorAKCaSPa7O8InOvYvpQOPS5o9vGd9XYfjTX7fufA==", + "version": "9.41.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-9.41.0.tgz", + "integrity": "sha512-L4fp9ZdxGBpMk3o2YqABgiPHNoHyu9Enid7JNkCdWP8iUgk7dEiDvo/XoiWPTAeNbF6W8Nqu54635mq01es0NQ==", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/element": "^5.12.0", - "@wordpress/primitives": "^3.33.0" + "@wordpress/element": "^5.27.0", + "@wordpress/primitives": "^3.48.0" }, "engines": { "node": ">=12" } }, "node_modules/@wordpress/jest-console": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-7.18.0.tgz", - "integrity": "sha512-OjPGbU1HgjLVNCLW9ROmdkw/qhpFL6Svlfv1aUVBrq5z1nJ7SrjRMeBSq4LJloOhTasSV9z7w4mhHJkMkfolJg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-7.21.0.tgz", + "integrity": "sha512-o2vZRlwwJ6WoxRwnFFT5iZzfdc2d9MZvrtwB093RWPNcyK5qVtApji4VN/ieHijB4bjEHGalm0UKfKpt0EDlUQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", @@ -5143,12 +5086,12 @@ } }, "node_modules/@wordpress/jest-preset-default": { - "version": "11.18.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-11.18.0.tgz", - "integrity": "sha512-qwcDXfKkdBJnnsQAa0qkBsg94usGQCD914pWNeBg997qy/6zmVYVXpPjXoJXaC/lYbEIRAWGfry1RSiM6ZoC9g==", + "version": "11.21.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-11.21.0.tgz", + "integrity": "sha512-XAztKOROu02iBsz+Qosv/RYuPWB1XwwlU+FiA5Y68tRztrqFy4b/il+DFg4Jue/zXF7UECWUvosd5ow/GmKa6Q==", "dev": true, "dependencies": { - "@wordpress/jest-console": "^7.18.0", + "@wordpress/jest-console": "^7.21.0", "babel-jest": "^29.6.2" }, "engines": { @@ -5160,23 +5103,22 @@ } }, "node_modules/@wordpress/keycodes": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.47.0.tgz", - "integrity": "sha512-dmYpqCWUoCM290YA5ApES9nqz/0D1JngIlZtel+BvELf8fj/jctdsT5wDB7dVdvZCuyr5SF+1Od00DYbMbb5oA==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.50.0.tgz", + "integrity": "sha512-ykWpyCbgwcaT8i5kSfotYtd2oOHyMDpWEYR73InYrzEhl7pnS3wD7hi/KfeKLvMfYhbysUXlCVr6q/oH+qK/DQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "^4.47.0", - "change-case": "^4.1.2" + "@wordpress/i18n": "^4.50.0" }, "engines": { "node": ">=12" } }, "node_modules/@wordpress/npm-package-json-lint-config": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-4.32.0.tgz", - "integrity": "sha512-qyEnU9FoWpaa67pufu9fNmTCikiYhdKc4R01ffO+xX7wyJXMo0Z6EJog6ajU9E2+YL86AmAX+sO1CHuXcsxdbw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-4.35.0.tgz", + "integrity": "sha512-QmkhYM4/s+2r3RuolVRRmoUa5o3lFgcHA6I3A9akaSVGZr//4p2p+iXOGmNub9njgGlj7j8SAPN8GUsCO/VqZQ==", "dev": true, "engines": { "node": ">=14" @@ -5186,12 +5128,12 @@ } }, "node_modules/@wordpress/postcss-plugins-preset": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-4.31.0.tgz", - "integrity": "sha512-B6bHsCKxt25nkvWfIJH3l7kENKS20mpsiRIl5+CEES6kKfBwg4IPx+JyA/RPLFQcIQNtIYFft22p5bgT4VZcEg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-4.34.0.tgz", + "integrity": "sha512-OLQBSLE2q11Ik+WdcO2QfGr/O4X/zJYOGXNsychx/EaMamLzJInFcRL6kGbPX41zPINhadq5x2vFIZI2EC+Uyg==", "dev": true, "dependencies": { - "@wordpress/base-styles": "^4.38.0", + "@wordpress/base-styles": "^4.41.0", "autoprefixer": "^10.2.5" }, "engines": { @@ -5202,9 +5144,9 @@ } }, "node_modules/@wordpress/prettier-config": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-3.4.0.tgz", - "integrity": "sha512-6qawlZqqbe6NDY0txzsPZThRFAXzf0a891wI/A4KNWVKUXQwTluXWMtGZx3xlFtvkX+9ZHdoqXbWysGQztiBOQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-3.7.0.tgz", + "integrity": "sha512-JRTc5p7UxtcPkqdSDXSFJoJnVuS510uiRVz8anXEl5nuOx5p+SJAzi9QPrxTgOE8bN3wRABH4eIhfOcta4CFdg==", "dev": true, "engines": { "node": ">=14" @@ -5214,12 +5156,12 @@ } }, "node_modules/@wordpress/primitives": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.33.0.tgz", - "integrity": "sha512-tgGkoDaWFELSoVM3FCS8T16DclIHbC7P2i3j8eVcprYsbgsGR+gaob7qWjgGb954A/OtSfayp1UNwl2kKuPh/A==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.48.0.tgz", + "integrity": "sha512-uBoMxpl+FiZF6aRXH/+Hwol4EAL6QqlNSaGF1IzEwklFzdRF1m5wTM4vh21w8Bq7lgxiuAqyueY7X5u32v+zPw==", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/element": "^5.12.0", + "@wordpress/element": "^5.27.0", "classnames": "^2.3.1" }, "engines": { @@ -5227,24 +5169,24 @@ } }, "node_modules/@wordpress/scripts": { - "version": "26.18.0", - "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-26.18.0.tgz", - "integrity": "sha512-cL3CKlPbH+JOnkV4MtGFUDys3KNlp6tjwrGBcpXsYOEm55DYtdXNmkRXHIfiM5hxCWiuE0P0dR7o/6F3Nz3TGA==", + "version": "27.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-27.1.0.tgz", + "integrity": "sha512-jewyOxqaNrsct5R1NXv2lT8CA70vzrvpdZHYERCcH9LzKuvrcc32Telm9Jqso6ay1ZgHeIbjHSCd2+r2sBG7hw==", "dev": true, "dependencies": { "@babel/core": "^7.16.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@svgr/webpack": "^8.0.1", - "@wordpress/babel-preset-default": "^7.31.0", - "@wordpress/browserslist-config": "^5.30.0", - "@wordpress/dependency-extraction-webpack-plugin": "^4.30.0", - "@wordpress/e2e-test-utils-playwright": "^0.15.0", - "@wordpress/eslint-plugin": "^17.4.0", - "@wordpress/jest-preset-default": "^11.18.0", - "@wordpress/npm-package-json-lint-config": "^4.32.0", - "@wordpress/postcss-plugins-preset": "^4.31.0", - "@wordpress/prettier-config": "^3.4.0", - "@wordpress/stylelint-config": "^21.30.0", + "@wordpress/babel-preset-default": "^7.34.0", + "@wordpress/browserslist-config": "^5.33.0", + "@wordpress/dependency-extraction-webpack-plugin": "^5.1.0", + "@wordpress/e2e-test-utils-playwright": "^0.18.0", + "@wordpress/eslint-plugin": "^17.7.0", + "@wordpress/jest-preset-default": "^11.21.0", + "@wordpress/npm-package-json-lint-config": "^4.35.0", + "@wordpress/postcss-plugins-preset": "^4.34.0", + "@wordpress/prettier-config": "^3.7.0", + "@wordpress/stylelint-config": "^21.33.0", "adm-zip": "^0.5.9", "babel-jest": "^29.6.2", "babel-loader": "^8.2.3", @@ -5295,7 +5237,7 @@ "wp-scripts": "bin/wp-scripts.js" }, "engines": { - "node": ">=14", + "node": ">=18", "npm": ">=6.14.4" }, "peerDependencies": { @@ -5305,9 +5247,9 @@ } }, "node_modules/@wordpress/stylelint-config": { - "version": "21.30.0", - "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-21.30.0.tgz", - "integrity": "sha512-PlvXzYgjn7OUaVTy2bahSr6oL/eu1OdRWxrZfGVNxF4jRswND/ThqOEHIzxETNGTe0ggZOyY+40St4Swlo1zZQ==", + "version": "21.33.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-21.33.0.tgz", + "integrity": "sha512-DwjXrjRBva0tkYILvDV7rjl3VaKXxvchlxnFfFs6l2DWL/Qo31CJ+f2rVw4XSWuuWxY1EsyIn9tOBS9URloWTQ==", "dev": true, "dependencies": { "stylelint-config-recommended": "^6.0.0", @@ -5321,9 +5263,9 @@ } }, "node_modules/@wordpress/url": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.48.0.tgz", - "integrity": "sha512-12bjIBBGcA5X8RPvUURLJZzpB60O5DI3WxQVIBBKPF4Mv8nUmgT4uemGzf5/ble8lqzJVntyEhEWKPOxEbUbJg==", + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.51.0.tgz", + "integrity": "sha512-OjucjlP1763gfKbe8lv/k3RCisyX8AfNBrhASk7JqxAj6rFhb1ZZO7YmAgB2m+WoGB5v7fkOli0FZyDqISdYyg==", "dev": true, "dependencies": { "@babel/runtime": "^7.16.0", @@ -5334,9 +5276,9 @@ } }, "node_modules/@wordpress/warning": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.47.0.tgz", - "integrity": "sha512-lmpLNI8Si7HrSY0LBBtp7Z6NzAkh1y7yeJI0LZw17EsJ0MM5FSXqXJRrNY7L4tM8G/vv3OacUw1mRAZX7bzBRQ==", + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.50.0.tgz", + "integrity": "sha512-y7Zf48roDfiPgbRAWGXDwN3C8sfbEdneGq+HvXCW6rIeGYnDLdEkpX9i7RfultkFFPVeSP3FpMKVMkto2nbqzA==", "dev": true, "engines": { "node": ">=12" @@ -5910,9 +5852,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", "dev": true, "funding": [ { @@ -5929,9 +5871,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -6118,13 +6060,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -6141,25 +6083,41 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6231,9 +6189,9 @@ ] }, "node_modules/basic-ftp": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", - "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", + "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", "dev": true, "engines": { "node": ">=10.0.0" @@ -6254,15 +6212,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -6399,18 +6348,6 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6574,21 +6511,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, - "dependencies": { - "run-applescript": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -6726,9 +6648,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", "dev": true, "funding": [ { @@ -6981,9 +6903,9 @@ "dev": true }, "node_modules/classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "node_modules/clean-stack": { "version": "2.2.0", @@ -7050,16 +6972,16 @@ } }, "node_modules/cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, "dependencies": { "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" + "string-width": "^7.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7077,19 +6999,65 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" }, "engines": { "node": ">=12" }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -7568,9 +7536,9 @@ } }, "node_modules/core-js": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.3.tgz", - "integrity": "sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", "dev": true, "hasInstallScript": true, "funding": { @@ -7579,12 +7547,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", - "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -7989,9 +7957,9 @@ "dev": true }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cwd": { "version": "0.10.0", @@ -8007,15 +7975,14 @@ } }, "node_modules/cypress": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.1.tgz", - "integrity": "sha512-k1Wl5PQcA/4UoTffYKKaxA0FJKwg8yenYNYRzLt11CUR0Kln+h7Udne6mdU1cUIdXBDTVZWtmiUjzqGs7/pEpw==", + "version": "13.6.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz", + "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==", "dev": true, "hasInstallScript": true, "dependencies": { "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -8077,9 +8044,9 @@ } }, "node_modules/cypress-mochawesome-reporter": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/cypress-mochawesome-reporter/-/cypress-mochawesome-reporter-3.7.0.tgz", - "integrity": "sha512-aeC5hpYJ/cS0M1PvIBfkyW3+yNIOgrFrI+ijEZZxsovGWqhSankCcias88igjiyzc+6mjFWnIXsd5NuRVF5nwA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/cypress-mochawesome-reporter/-/cypress-mochawesome-reporter-3.8.1.tgz", + "integrity": "sha512-oqtyDE4OOd5D7uas4+ljIb3vkO4gHWErhWKV7TbNF20YweiHWmzuOmS6L0MGk3J6IF6VbfO4h86kSa0sNsaKUg==", "dev": true, "dependencies": { "commander": "^10.0.1", @@ -8133,15 +8100,6 @@ "ally.js": "^1.4.1" } }, - "node_modules/cypress/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/cypress/node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -8411,41 +8369,19 @@ "node": ">=0.10.0" } }, - "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", - "dev": true, - "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" + "execa": "^5.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 10" } }, - "node_modules/default-browser/node_modules/cross-spawn": { + "node_modules/default-gateway/node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", @@ -8459,194 +8395,10 @@ "node": ">= 8" } }, - "node_modules/default-browser/node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-browser/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/default-browser/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/default-browser/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/default-browser/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/default-gateway/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/default-gateway/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.3", @@ -8972,14 +8724,26 @@ } }, "node_modules/docker-compose": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.22.2.tgz", - "integrity": "sha512-iXWb5+LiYmylIMFXvGTYsjI1F+Xyx78Jm/uj1dxwwZLbWkUdH6yOXY5Nr3RjbYX15EgbGJCq78d29CmWQQQMPg==", + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.3.tgz", + "integrity": "sha512-x3/QN3AIOMe7j2c8f/jcycizMft7dl8MluoB9OGPAYCyKHHiPUFqI9GjCcsU0kYy24vYKMCcfR6+5ZaEyQlrxg==", "dev": true, + "dependencies": { + "yaml": "^2.2.2" + }, "engines": { "node": ">= 6.0.0" } }, + "node_modules/docker-compose/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -9087,12 +8851,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -9428,15 +9186,15 @@ } }, "node_modules/eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", - "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.55.0", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -9558,9 +9316,9 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", - "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { "array-includes": "^3.1.7", @@ -9579,7 +9337,7 @@ "object.groupby": "^1.0.1", "object.values": "^1.1.7", "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -9619,9 +9377,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz", - "integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==", + "version": "27.6.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", + "integrity": "sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -9789,9 +9547,9 @@ "dev": true }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz", - "integrity": "sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==", + "version": "46.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", + "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.41.0", @@ -9802,13 +9560,13 @@ "esquery": "^1.5.0", "is-builtin-module": "^3.2.1", "semver": "^7.5.4", - "spdx-expression-parse": "^3.0.1" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": ">=16" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-plugin-jsdoc/node_modules/lru-cache": { @@ -9890,23 +9648,24 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.5" + "synckit": "^0.8.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/prettier" + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", + "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -10078,9 +9837,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -11110,6 +10869,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -12307,15 +12078,12 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-generator-fn": { @@ -12354,39 +12122,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -14250,39 +13985,75 @@ } }, "node_modules/lint-staged": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.2.tgz", - "integrity": "sha512-71gSwXKy649VrSU09s10uAT0rWCcY3aewhMaHyl2N84oBk4Xs9HgxvUp3AYu+bNsK4NrOYYxvSgg7FyGJ+jGcA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.0.tgz", + "integrity": "sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ==", "dev": true, "dependencies": { - "chalk": "5.2.0", - "cli-truncate": "^3.1.0", - "commander": "^10.0.0", - "debug": "^4.3.4", - "execa": "^7.0.0", - "lilconfig": "2.1.0", - "listr2": "^5.0.7", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-inspect": "^1.12.3", - "pidtree": "^0.6.0", - "string-argv": "^0.3.1", - "yaml": "^2.2.2" + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.0", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=18.12.0" }, "funding": { "url": "https://opencollective.com/lint-staged" } }, + "node_modules/lint-staged/node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/lint-staged/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -14291,13 +14062,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/lint-staged/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lint-staged/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/lint-staged/node_modules/cross-spawn": { @@ -14314,57 +14100,75 @@ "node": ">= 8" } }, + "node_modules/lint-staged/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/lint-staged/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/lint-staged/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", - "signal-exit": "^3.0.7", + "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" }, "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + "node": ">=16.17" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/lint-staged/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lint-staged/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, "engines": { - "node": ">=14.18.0" + "node": ">=16.17.0" } }, "node_modules/lint-staged/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lint-staged/node_modules/is-stream": { @@ -14379,44 +14183,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lint-staged/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/lint-staged/node_modules/listr2": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.8.tgz", - "integrity": "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.0.tgz", + "integrity": "sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg==", "dev": true, "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.19", - "log-update": "^4.0.0", - "p-map": "^4.0.0", + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", "rfdc": "^1.3.0", - "rxjs": "^7.8.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^9.0.0" }, "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/lint-staged/node_modules/listr2/node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "node_modules/lint-staged/node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", "dev": true, "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -14435,9 +14241,9 @@ } }, "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -14476,30 +14282,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/p-map": { + "node_modules/lint-staged/node_modules/restore-cursor": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dev": true, "dependencies": { - "aggregate-error": "^3.0.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/lint-staged/node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lint-staged/node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "dependencies": { - "tslib": "^2.1.0" + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lint-staged/node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/lint-staged/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -14521,18 +14349,64 @@ "node": ">=8" } }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/lint-staged/node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/string-width": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/lint-staged/node_modules/strip-final-newline": { @@ -14541,7 +14415,19 @@ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, "engines": { - "node": ">=12" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -14562,10 +14448,27 @@ "node": ">= 8" } }, + "node_modules/lint-staged/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true, "engines": { "node": ">= 14" @@ -14614,15 +14517,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/listr2/node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -14799,32 +14693,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -15706,6 +15574,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/mochawesome-json-to-md/-/mochawesome-json-to-md-0.7.2.tgz", "integrity": "sha512-dxh+o73bhC6nEph6fNky9wy35R+2oK3ueXwAlJ/COAanlFgu8GuvGzQ00VNO4PPYhYGDsO4vbt4QTcMA3lv25g==", + "deprecated": "🙌 Thanks for using it. We recommend upgrading to the newer version, 1.x.x. Check out https://www.npmjs.com/package/mochawesome-json-to-md for details.", "dev": true, "dependencies": { "yargs": "^17.0.1" @@ -16259,9 +16128,9 @@ } }, "node_modules/npm-package-json-lint/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/npm-package-json-lint/node_modules/lru-cache": { @@ -17157,13 +17026,13 @@ "dev": true }, "node_modules/playwright": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", - "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", + "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", "dev": true, "peer": true, "dependencies": { - "playwright-core": "1.40.1" + "playwright-core": "1.41.1" }, "bin": { "playwright": "cli.js" @@ -17188,9 +17057,9 @@ } }, "node_modules/playwright/node_modules/playwright-core": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", - "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", + "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", "dev": true, "peer": true, "bin": { @@ -18867,115 +18736,6 @@ "node": ">=10.0.0" } }, - "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/run-applescript/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/run-applescript/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/run-applescript/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-applescript/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-applescript/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -19057,13 +18817,13 @@ "dev": true }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -19095,15 +18855,18 @@ ] }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -19582,33 +19345,22 @@ } }, "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -19811,16 +19563,26 @@ "spdx-license-ids": "^3.0.0" } }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", "dev": true }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", @@ -19959,9 +19721,9 @@ } }, "node_modules/streamx": { - "version": "2.15.5", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz", - "integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==", + "version": "2.15.6", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", + "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", "dev": true, "dependencies": { "fast-fifo": "^1.1.0", @@ -20025,15 +19787,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/string-width/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -20482,12 +20235,12 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", - "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", "dev": true, "dependencies": { - "@pkgr/utils": "^2.4.2", + "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" }, "engines": { @@ -20529,38 +20282,12 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/table/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/table/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/table/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/taffydb": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", @@ -20659,9 +20386,9 @@ } }, "node_modules/terser": { - "version": "5.17.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", - "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -20677,16 +20404,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -20816,18 +20543,6 @@ "@popperjs/core": "^2.9.0" } }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -20979,9 +20694,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", @@ -21232,12 +20947,6 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -21476,6 +21185,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/validate-npm-package-name": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", @@ -21607,9 +21326,9 @@ } }, "node_modules/web-vitals": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz", - "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz", + "integrity": "sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==", "dev": true }, "node_modules/webidl-conversions": { @@ -21622,19 +21341,19 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.0.tgz", + "integrity": "sha512-bdmyXRCXeeNIePv6R6tGPyy20aUobw4Zy8r0LUS2EWO+U+Ke/gYDgsCh7bl5rB6jPpr4r0SZa6dPxBxLooDT3w==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -21648,7 +21367,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, diff --git a/package.json b/package.json index a73418c6d..c472c8b76 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "makepot": "wpi18n makepot && echo '.pot file updated'", "build:docs": "rm -rf docs && jsdoc -c hookdoc-conf.json classifai.php includes", "cypress:open": "cypress open --config-file tests/cypress/config.config.js", - "cypress:run": "cypress run --config-file tests/cypress/config.config.js", + "cypress:run": "cypress run --browser chrome --config-file tests/cypress/config.config.js", "env": "wp-env", "env:start": "wp-env start", "env:stop": "wp-env stop", @@ -37,24 +37,24 @@ }, "devDependencies": { "@10up/cypress-wp-utils": "^0.2.0", - "@wordpress/env": "^8.13.0", - "@wordpress/scripts": "^26.18.0", - "cypress": "^13.6.1", + "@wordpress/env": "^9.2.0", + "@wordpress/scripts": "^27.1.0", + "cypress": "^13.6.4", "cypress-file-upload": "^5.0.8", - "cypress-mochawesome-reporter": "^3.7.0", + "cypress-mochawesome-reporter": "^3.8.1", "cypress-plugin-tab": "^1.0.5", "husky": "^8.0.3", "jsdoc": "^3.6.11", - "lint-staged": "^13.2.2", + "lint-staged": "^15.2.0", "mochawesome-json-to-md": "^0.7.2", "node-wp-i18n": "^1.2.7", "svg-react-loader": "^0.4.6", - "webpack": "^5.86.0", + "webpack": "^5.90.0", "webpack-cli": "^5.1.4", "wp-hookdoc": "^0.2.0" }, "dependencies": { - "@wordpress/icons": "^9.26.0", + "@wordpress/icons": "^9.41.0", "choices.js": "^10.2.0", "tippy.js": "^6.3.7" } diff --git a/src/js/admin.js b/src/js/admin.js index 8fcf28084..a2d76c721 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -32,9 +32,7 @@ document.addEventListener( 'DOMContentLoaded', function () { ( () => { const $toggler = document.getElementById( 'classifai-waston-cred-toggle' ); - const $userField = document.getElementById( - 'classifai-settings-watson_username' - ); + const $userField = document.getElementById( 'username' ); if ( $toggler === null || $userField === null ) { return; @@ -48,31 +46,27 @@ document.addEventListener( 'DOMContentLoaded', function () { $userFieldWrapper = $userField.closest( '.classifai-setup-form-field' ); } - if ( - document - .getElementById( 'classifai-settings-watson_password' ) - .closest( 'tr' ) - ) { + if ( document.getElementById( 'password' ).closest( 'tr' ) ) { [ $passwordFieldTitle ] = document - .getElementById( 'classifai-settings-watson_password' ) + .getElementById( 'password' ) .closest( 'tr' ) .getElementsByTagName( 'label' ); } else if ( document - .getElementById( 'classifai-settings-watson_password' ) + .getElementById( 'password' ) .closest( '.classifai-setup-form-field' ) ) { [ $passwordFieldTitle ] = document - .getElementById( 'classifai-settings-watson_password' ) + .getElementById( 'password' ) .closest( '.classifai-setup-form-field' ) .getElementsByTagName( 'label' ); } $toggler.addEventListener( 'click', ( e ) => { e.preventDefault(); - $userFieldWrapper.classList.toggle( 'hidden' ); + $userFieldWrapper.classList.toggle( 'hide-username' ); - if ( $userFieldWrapper.classList.contains( 'hidden' ) ) { + if ( $userFieldWrapper.classList.contains( 'hide-username' ) ) { $toggler.innerText = ClassifAI.use_password; $passwordFieldTitle.innerText = ClassifAI.api_key; $userField.value = 'apikey'; @@ -392,3 +386,28 @@ document.addEventListener( 'DOMContentLoaded', function () { return $newPromptFieldset; } } )(); + +/** + * Feature-first refactor settings field: + * @param {Object} $ jQuery object + */ +( function ( $ ) { + $( function () { + const providerSelectEl = $( 'select#provider' ); + + providerSelectEl.on( 'change', function () { + const providerId = $( this ).val(); + const providerRows = $( '.classifai-provider-field' ); + const providerClass = `.provider-scope-${ providerId }`; + + providerRows.addClass( 'hidden' ); + providerRows.find( ':input' ).prop( 'disabled', true ); + + $( providerClass ).removeClass( 'hidden' ); + $( providerClass ).find( ':input' ).prop( 'disabled', false ); + } ); + + // Trigger 'change' on page load. + providerSelectEl.trigger( 'change' ); + } ); +} )( jQuery ); diff --git a/src/js/editor-ocr.js b/src/js/editor-ocr.js index 1daab3da2..3d5f8b514 100644 --- a/src/js/editor-ocr.js +++ b/src/js/editor-ocr.js @@ -4,7 +4,7 @@ /* eslint-disable @wordpress/no-unused-vars-before-return */ import { select, dispatch, subscribe } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; -import { apiFetch } from '@wordpress/api-fetch'; +import apiFetch from '@wordpress/api-fetch'; import { addFilter } from '@wordpress/hooks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { BlockControls } from '@wordpress/block-editor'; diff --git a/src/js/gutenberg-plugins/content-resizing-plugin.js b/src/js/gutenberg-plugins/content-resizing-plugin.js index 7753a86d3..af6d8e5d4 100644 --- a/src/js/gutenberg-plugins/content-resizing-plugin.js +++ b/src/js/gutenberg-plugins/content-resizing-plugin.js @@ -185,7 +185,7 @@ const ContentResizingPlugin = () => { */ async function getResizedContent() { let __textArray = []; - const apiUrl = `${ wpApiSettings.root }classifai/v1/openai/resize-content`; + const apiUrl = `${ wpApiSettings.root }classifai/v1/resize-content`; const postId = select( editorStore ).getCurrentPostId(); const formData = new FormData(); @@ -330,7 +330,7 @@ const ContentResizingPlugin = () => {
- + ); diff --git a/src/js/gutenberg-plugins/post-status-info.js b/src/js/gutenberg-plugins/post-status-info.js index 5fa351685..d386b9cd7 100644 --- a/src/js/gutenberg-plugins/post-status-info.js +++ b/src/js/gutenberg-plugins/post-status-info.js @@ -128,7 +128,7 @@ const PostStatusInfo = () => { { ! isLoading && data && } { ! isLoading && error && } { ! isLoading && ( - + ) } ) } diff --git a/src/js/language-processing.js b/src/js/language-processing.js index 43d7e568d..d5a18b393 100644 --- a/src/js/language-processing.js +++ b/src/js/language-processing.js @@ -4,20 +4,14 @@ import '../scss/language-processing.scss'; ( () => { let featureStatuses = {}; - const nonceElementNLU = document.getElementById( - 'classifai-previewer-watson_nlu-nonce' - ); - - const nonceElementEmbeddings = document.getElementById( - 'classifai-previewer-openai_embeddings-nonce' - ); + const nonceEl = document.getElementById( 'classifai-previewer-nonce' ); - if ( ! nonceElementNLU && ! nonceElementEmbeddings ) { + if ( ! nonceEl ) { return; } const previewWatson = () => { - if ( ! nonceElementNLU ) { + if ( ! nonceEl ) { return; } @@ -27,22 +21,14 @@ import '../scss/language-processing.scss'; getClassifierDataBtn.addEventListener( 'click', showPreviewWatson ); /** Previewer nonce. */ - const previewerNonce = nonceElementNLU.value; + const previewerNonce = nonceEl.value; /** Feature statuses. */ featureStatuses = { - categoriesStatus: document.getElementById( - 'classifai-settings-category' - ).checked, - keywordsStatus: document.getElementById( - 'classifai-settings-keyword' - ).checked, - entitiesStatus: document.getElementById( - 'classifai-settings-entity' - ).checked, - conceptsStatus: document.getElementById( - 'classifai-settings-concept' - ).checked, + categoriesStatus: document.getElementById( 'category' ).checked, + keywordsStatus: document.getElementById( 'keyword' ).checked, + entitiesStatus: document.getElementById( 'entity' ).checked, + conceptsStatus: document.getElementById( 'concept' ).checked, }; const plurals = { @@ -53,24 +39,22 @@ import '../scss/language-processing.scss'; }; document - .querySelectorAll( - '#classifai-settings-category, #classifai-settings-keyword, #classifai-settings-entity, #classifai-settings-concept' - ) + .querySelectorAll( '#category, #keyword, #entity, #concept' ) .forEach( ( item ) => { item.addEventListener( 'change', ( e ) => { - if ( 'classifai-settings-category' === e.target.id ) { + if ( 'category' === e.target.id ) { featureStatuses.categoriesStatus = e.target.checked; } - if ( 'classifai-settings-keyword' === e.target.id ) { + if ( 'keyword' === e.target.id ) { featureStatuses.keywordsStatus = e.target.checked; } - if ( 'classifai-settings-entity' === e.target.id ) { + if ( 'entity' === e.target.id ) { featureStatuses.entitiesStatus = e.target.checked; } - if ( 'classifai-settings-concept' === e.target.id ) { + if ( 'concept' === e.target.id ) { featureStatuses.conceptsStatus = e.target.checked; } @@ -100,23 +84,16 @@ import '../scss/language-processing.scss'; function showPreviewWatson( e ) { /** Category thresholds. */ const categoryThreshold = Number( - document.querySelector( - '#classifai-settings-category_threshold' - ).value + document.querySelector( '#category_threshold' ).value ); const keywordThreshold = Number( - document.querySelector( - '#classifai-settings-keyword_threshold' - ).value + document.querySelector( '#keyword_threshold' ).value ); const entityThreshold = Number( - document.querySelector( '#classifai-settings-entity_threshold' ) - .value + document.querySelector( '#entity_threshold' ).value ); const conceptThreshold = Number( - document.querySelector( - '#classifai-settings-concept_threshold' - ).value + document.querySelector( '#concept_threshold' ).value ); const postId = document.getElementById( @@ -222,7 +199,7 @@ import '../scss/language-processing.scss'; previewWatson(); const previewEmbeddings = () => { - if ( ! nonceElementEmbeddings ) { + if ( ! nonceEl ) { return; } @@ -232,7 +209,7 @@ import '../scss/language-processing.scss'; getClassifierDataBtn.addEventListener( 'click', showPreviewEmeddings ); /** Previewer nonce. */ - const previewerNonce = nonceElementEmbeddings.value; + const previewerNonce = nonceEl.value; /** * Live preview features. @@ -372,15 +349,12 @@ import '../scss/language-processing.scss'; * @param {Object} event Choices.js's 'search' event object. */ function searchPosts( event ) { - const nonceElement = nonceElementEmbeddings - ? nonceElementEmbeddings - : nonceElementNLU; - if ( ! nonceElement ) { + if ( ! nonceEl ) { return; } /** Previewer nonce. */ - const previewerNonce = nonceElement.value; + const previewerNonce = nonceEl.value; /* * Post types. diff --git a/src/js/media.js b/src/js/media.js index 5dcfb0dc4..d45c25733 100644 --- a/src/js/media.js +++ b/src/js/media.js @@ -136,7 +136,7 @@ import { __ } from '@wordpress/i18n'; transcribeButton.addEventListener( 'click', ( e ) => handleClick( { button: e.target, - endpoint: '/classifai/v1/openai/generate-transcript/', + endpoint: '/classifai/v1/generate-transcript/', callback: ( resp ) => { if ( resp ) { const textField = diff --git a/src/js/openai/classic-editor-excerpt-generator.js b/src/js/openai/classic-editor-excerpt-generator.js index 86e50d76c..e093271d8 100644 --- a/src/js/openai/classic-editor-excerpt-generator.js +++ b/src/js/openai/classic-editor-excerpt-generator.js @@ -43,7 +43,7 @@ const classifaiExcerptData = window.classifaiGenerateExcerpt || {}; // Append disable feature link. if ( ClassifAI?.opt_out_enabled_features?.includes( - 'excerpt_generation' + 'feature_excerpt_generation' ) ) { $( '', { diff --git a/src/js/openai/classic-editor-title-generator.js b/src/js/openai/classic-editor-title-generator.js index 6575326dd..ed7a3162a 100644 --- a/src/js/openai/classic-editor-title-generator.js +++ b/src/js/openai/classic-editor-title-generator.js @@ -128,7 +128,7 @@ const scriptData = classifaiChatGPTData.enabledFeatures.reduce( // Append disable feature link. if ( ClassifAI?.opt_out_enabled_features?.includes( - 'title_generation' + 'feature_title_generation' ) ) { $( '', { diff --git a/src/js/post-excerpt/panel.js b/src/js/post-excerpt/panel.js index e3d4ba1fe..e4ba898c3 100644 --- a/src/js/post-excerpt/panel.js +++ b/src/js/post-excerpt/panel.js @@ -83,7 +83,7 @@ function PostExcerpt( { excerpt, onUpdateExcerpt } ) { disabled={ isLoading } data-id={ postId } onClick={ () => - buttonClick( '/classifai/v1/openai/generate-excerpt/' ) + buttonClick( '/classifai/v1/generate-excerpt/' ) } > { buttonText } @@ -106,7 +106,7 @@ function PostExcerpt( { excerpt, onUpdateExcerpt } ) { { error } ) } - + ); } diff --git a/src/scss/admin.scss b/src/scss/admin.scss index 7a87b94f2..62ef171e2 100644 --- a/src/scss/admin.scss +++ b/src/scss/admin.scss @@ -276,6 +276,10 @@ input.classifai-button { outline: 2px solid transparent; } + .components-form-token-field__suggestion { + box-sizing: border-box; + } + .components-form-token-field__suggestion.is-selected { background: var(--classifai-highlight-color); } @@ -363,6 +367,10 @@ input.classifai-button { margin-bottom: 4px; } + .hide-username { + display: none; + } + .components-form-token-field__input-container input[type=text].components-form-token-field__input { height: auto; font-size: 14px; @@ -515,9 +523,11 @@ input.classifai-button { margin-left: -10px; } - &__wrapper { + &__wrapper, + .classifai-wrap { max-width: 1232px; margin: 0px auto; + padding: 0px; } &__content { @@ -655,21 +665,23 @@ input.classifai-button { } .classifai-setup-form { - .classifai-step3-content & { margin: 0 auto; - max-width: 480px; + } + + .classifai-setup-form-field-label { + text-align: left; } .classifai-setup-form-field { - margin-top: 40px; + padding: 0px; } - label { + .classifai-setup-form-field-label > label { display: block; font-weight: 700; text-transform: uppercase; - margin-bottom: 20px; + margin-bottom: 0px; } input[type="text"], @@ -687,28 +699,45 @@ input.classifai-button { .classifai-step3-content { margin: 0 auto; + max-width: 840px; + width: 100%; .classifai-setup-title { text-align: center; margin-bottom: 12px; } - .classifai-setup-form, - .classifai-setup-footer { - margin: 0 auto; - max-width: 480px; + .classifai-setup-form { + padding-left: 48px; + width: 70%; + box-sizing: border-box; + + @media screen and (max-width: 782px) { + padding-left: 18px; + } + + @media screen and (max-width: 600px) { + width: 100%; + padding-left: 0px; + margin-bottom: 20px; + } } .classifai-setup-footer { - margin-top: 40px; + margin-top: 20px; } } .classifai-tabs { display: block; + width: 30%; + box-sizing: border-box; + + @media screen and (max-width: 600px) { + width: 100%; + } &.tabs-center { - margin: auto; margin-bottom: 24px; } @@ -720,12 +749,11 @@ input.classifai-button { a.tab { text-decoration: none; position: relative; - display: inline-block; - transition: all ease .3s; + display: block; + transition: all ease 0.3s; padding: 16px 12px; transform: translate3d(0, 0, 0); color: #1d2327; - white-space: nowrap; cursor: pointer; font-size: 14px; @@ -737,24 +765,10 @@ input.classifai-button { color: var(--classifai-admin-theme-color); } - &:after { - transition: all .3s cubic-bezier(1, 0, 0, 1); - will-change: transform, box-shadow, opacity; - position: absolute; - content: ''; - height: 3px; - bottom: 0px; - left: 0px; - right: 0px; - border-radius: 3px 3px 0px 0px; - background: var(--classifai-admin-theme-color); - box-shadow: 0px 4px 10px 3px rgba(var(--classifai-admin-theme-color--rgb), .15); - opacity: 0; - transform: scale(0, 1); - } - &.active { - color: var(--classifai-admin-theme-color); + background: #f0f0f0; + box-shadow: none; + border-radius: 4px; font-weight: 600; &:after { @@ -765,6 +779,12 @@ input.classifai-button { } } + .classifai-providers-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + p.classifai-setup-error { text-align: center; color: red; @@ -773,6 +793,16 @@ input.classifai-button { span.description { font-size: 12px; } + + .dashicons-clock { + color: #a7aaad; + } + .dashicons-yes-alt{ + color: #48BE1E; + } + .dashicons-warning { + color: #dba617; + } } } diff --git a/tests/Classifai/Admin/SavePostHandlerTest.php b/tests/Classifai/Admin/SavePostHandlerTest.php deleted file mode 100644 index 80a451aff..000000000 --- a/tests/Classifai/Admin/SavePostHandlerTest.php +++ /dev/null @@ -1,117 +0,0 @@ - [ - 'watson_url' => 'url', - 'watson_username' => 'username', - 'watson_password' => 'password', - ], - ]; - - /** - * setup method - */ - function set_up() { - parent::set_up(); - - $this->save_post_handler = new SavePostHandler(); - } - - function add_options() { - update_option( 'classifai_configured', true ); - update_option( 'classifai_watson_nlu', $this->settings ); - } - - function test_is_rest_route() { - global $wp_filter; - - $saved_filters = $wp_filter['classifai_rest_bases'] ?? null; - unset( $wp_filter['classifai_rest_bases'] ); - - $this->assertEquals( false, $this->save_post_handler->is_rest_route() ); - - $_SERVER['REQUEST_URI'] = '/wp-json/wp/v2/users/me'; - $this->assertEquals( false, $this->save_post_handler->is_rest_route() ); - - $_SERVER['REQUEST_URI'] = '/wp-json/wp/v2/posts/1'; - $this->assertEquals( true, $this->save_post_handler->is_rest_route() ); - - $_SERVER['REQUEST_URI'] = '/wp-json/wp/v2/pages/1'; - $this->assertEquals( true, $this->save_post_handler->is_rest_route() ); - - $_SERVER['REQUEST_URI'] = '/wp-json/wp/v2/custom/1'; - $this->assertEquals( false, $this->save_post_handler->is_rest_route() ); - - if ( ! is_null( $saved_filters ) ) { - $wp_filter['classifai_rest_bases'] = $saved_filters; - } - - add_filter( - 'classifai_rest_bases', - function( $bases ) { - $bases[] = 'custom'; - return $bases; - } - ); - $this->assertEquals( true, $this->save_post_handler->is_rest_route() ); - } - - function test_rest_route_register() { - - $_SERVER['REQUEST_URI'] = '/wp-json/wp/v2/posts/1'; - - $this->assertEquals( false, $this->save_post_handler->can_register() ); - - $this->add_options(); - - $this->assertEquals( true, $this->save_post_handler->can_register() ); - } - - function test_is_admin_register() { - - set_current_screen( 'edit.php' ); - - $this->assertEquals( false, $this->save_post_handler->can_register() ); - - $this->add_options(); - - $this->assertEquals( true, $this->save_post_handler->can_register() ); - } - - function test_custom_register() { - - define( 'DOING_CRON', true ); - - $this->assertEquals( false, $this->save_post_handler->can_register() ); - - add_filter( - 'classifai_should_register_save_post_handler', - function( $should_register ) { - if ( defined( 'DOING_CRON' ) && DOING_CRON ) { - return true; - } - return $should_register; - } - ); - - $this->assertEquals( true, $this->save_post_handler->can_register() ); - } -} diff --git a/tests/Classifai/Azure/ComputerVisionTest.php b/tests/Classifai/Azure/ComputerVisionTest.php index 14b74d326..867bf5cef 100644 --- a/tests/Classifai/Azure/ComputerVisionTest.php +++ b/tests/Classifai/Azure/ComputerVisionTest.php @@ -23,35 +23,29 @@ class ComputerVisionTest extends WP_UnitTestCase { function set_up() { parent::set_up(); - $this->provider = new ComputerVision( 'service_name' ); + $this->provider = new ComputerVision( new \Classifai\Features\DescriptiveTextGenerator() ); } /** * Tests the function providing debug information. */ - public function test_get_provider_debug_information() { + public function test_get_debug_information() { $this->assertEquals( [ - 'Authenticated', - 'API URL', - 'Caption threshold', - 'Latest response - Image Scan', - 'Latest response - Smart Cropping', - 'Latest response - OCR', + 'Generate descriptive text', + 'Confidence threshold', + 'Latest response:', ], - array_keys( $this->provider->get_provider_debug_information() ) + array_keys( $this->provider->get_debug_information() ) ); $this->assertEquals( [ - 'Authenticated' => 'yes', - 'API URL' => 'my-azure-url.com', - 'Caption threshold' => 77, - 'Latest response - Image Scan' => 'N/A', - 'Latest response - Smart Cropping' => 'N/A', - 'Latest response - OCR' => 'N/A', + 'Generate descriptive text' => '0, 0, 0', + 'Confidence threshold' => 75, + 'Latest response:' => 'N/A', ], - $this->provider->get_provider_debug_information( + $this->provider->get_debug_information( [ 'url' => 'my-azure-url.com', 'caption_threshold' => 77, diff --git a/tests/Classifai/HelpersTest.php b/tests/Classifai/HelpersTest.php index 67172b34d..8b063cb9a 100644 --- a/tests/Classifai/HelpersTest.php +++ b/tests/Classifai/HelpersTest.php @@ -2,6 +2,12 @@ namespace Classifai; +use function Classifai\Providers\Watson\get_username; +use function Classifai\Providers\Watson\get_password; +use function Classifai\Providers\Watson\get_supported_post_types; +use function Classifai\Providers\Watson\get_feature_threshold; +use function Classifai\Providers\Watson\get_feature_taxonomy; + /** * @group helpers */ @@ -98,7 +104,7 @@ function test_it_knows_configured_username() { ] ] ); - $actual = get_watson_username(); + $actual = get_username(); $this->assertEquals( 'foo', $actual ); } @@ -110,7 +116,7 @@ function test_it_knows_configured_password() { ] ] ); - $actual = get_watson_password(); + $actual = get_password(); $this->assertEquals( 'foo', $actual ); } diff --git a/tests/Classifai/PostClassifierTest.php b/tests/Classifai/PostClassifierTest.php index 9e1f420d6..a54205e26 100644 --- a/tests/Classifai/PostClassifierTest.php +++ b/tests/Classifai/PostClassifierTest.php @@ -1,6 +1,6 @@ classifier->get_api_request(); - $this->assertInstanceOf( 'Classifai\Watson\APIRequest', $actual ); + $this->assertInstanceOf( 'Classifai\Providers\Watson\APIRequest', $actual ); } function test_it_has_a_normalizer() { $actual = $this->classifier->get_normalizer(); - $this->assertInstanceOf( 'Classifai\Watson\Normalizer', $actual ); + $this->assertInstanceOf( 'Classifai\Normalizer', $actual ); } function test_it_has_a_linker() { $actual = $this->classifier->get_linker(); - $this->assertInstanceOf( 'Classifai\Watson\Linker', $actual ); + $this->assertInstanceOf( 'Classifai\Providers\Watson\Linker', $actual ); } function test_it_has_a_classifier() { $actual = $this->classifier->get_classifier(); - $this->assertInstanceOf( 'Classifai\Watson\Classifier', $actual ); + $this->assertInstanceOf( 'Classifai\Providers\Watson\Classifier', $actual ); } function test_it_can_link_post() { diff --git a/tests/Classifai/Providers/Azure/ComputerVisionTest.php b/tests/Classifai/Providers/Azure/ComputerVisionTest.php index fd3fdb460..4f0f61890 100644 --- a/tests/Classifai/Providers/Azure/ComputerVisionTest.php +++ b/tests/Classifai/Providers/Azure/ComputerVisionTest.php @@ -8,8 +8,6 @@ use \WP_UnitTestCase; use Classifai\Providers\Azure\ComputerVision; -use function Classifai\get_feature_default_settings; - /** * Class ComputerVisionTest * @package Classifai\Tests\Providers\Azure; @@ -39,13 +37,7 @@ public function get_computer_vision() : ComputerVision { * @covers ::smart_crop_image */ public function test_smart_crop_image() { - $this->assertEquals( - 'non-array-data', - $this->get_computer_vision()->smart_crop_image( 'non-array-data', 999999 ) - ); - - $this->assertEquals( - [ 'no-smart-cropping' => 1 ], + $this->assertWPError( $this->get_computer_vision()->smart_crop_image( [ 'no-smart-cropping' => 1 ], 999999 @@ -59,29 +51,15 @@ public function test_smart_crop_image() { }; add_filter( 'filesystem_method', $filter_file_system_method ); - $this->assertEquals( + $this->assertWPError( $this->get_computer_vision()->smart_crop_image( [ 'not-direct-file-system-method' => 1 ], - $this->get_computer_vision()->smart_crop_image( - [ 'not-direct-file-system-method' => 1 ], - 999999 - ) - ); + 999999 + ) ); remove_filter( 'filesystem_method', $filter_file_system_method ); - // Test that SmartCropping is initiated and runs, as will be indicated in the coverage report, though it won't - // actually do anything because the data and attachment are invalid. - $this->assertEquals( - [ 'my-data' => 1 ], - $this->get_computer_vision()->smart_crop_image( - [ 'my-data' => 1 ], - 999999 - ) - ); - remove_filter( 'classifai_should_smart_crop_image', '__return_true' ); } - /** * Ensure that settings returns default settings array if the `classifai_computer_vision` is not set. */ @@ -89,35 +67,31 @@ public function test_no_computer_vision_option_set() { delete_option( 'classifai_computer_vision' ); $defaults = []; - $features = $this->get_computer_vision()->get_features() ?? []; - foreach ( $features as $feature => $title ) { - $defaults = array_merge( - $defaults, - get_feature_default_settings( $feature ) - ); - } $expected = array_merge( $defaults, [ - 'valid' => false, - 'url' => '', - 'api_key' => '', - 'enable_image_captions' => array( - 'alt' => 0, - 'caption' => 0, + 'status' => '0', + 'role_based_access' => '1', + 'roles' => [], + 'user_based_access' => 'no', + 'users' => [], + 'user_based_opt_out' => 'no', + 'descriptive_text_fields' => [ + 'alt' => 0, + 'caption' => 0, 'description' => 0, - ), - 'enable_image_tagging' => true, - 'enable_smart_cropping' => false, - 'enable_ocr' => false, - 'enable_read_pdf' => false, - 'caption_threshold' => 75, - 'tag_threshold' => 70, - 'image_tag_taxonomy' => 'classifai-image-tags', + ], + 'provider' => 'ms_computer_vision', + 'ms_computer_vision' => [ + 'endpoint_url' => '', + 'api_key' => '', + 'authenticated' => false, + 'descriptive_confidence_threshold' => 75, + ], ] ); - $settings = $this->get_computer_vision()->get_settings(); + $settings = ( new \Classifai\Features\DescriptiveTextGenerator() )->get_settings(); $this->assertSame( $expected, $settings ); } @@ -149,51 +123,45 @@ public function test_set_image_meta_data() { } public function test_alt_text_option_reformatting() { - add_option( 'classifai_computer_vision', array() ); + add_option( 'classifai_feature_descriptive_text_generator', array() ); $options = array( - 'valid' => false, - 'url' => '', - 'api_key' => '', - 'enable_image_captions' => '1', - 'enable_image_tagging' => '1', - 'enable_smart_cropping' => 'no', - 'enable_ocr' => 'no', - 'enable_read_pdf' => 'no', - 'caption_threshold' => 75, - 'tag_threshold' => 70, - 'image_tag_taxonomy' => 'classifai-image-tags', + 'status' => '1', + 'provider' => 'ms_computer_vision', + 'ms_computer_vision' => array( + 'endpoint_url' => '', + 'api_key' => '', + 'descriptive_confidence_threshold' => '75', + 'authenticated' => true, + ), + 'descriptive_text_fields' => array( + 'alt' => 'alt', + 'caption' => '0', + 'description' => '0', + ), ); - // Test with `enable_image_captions` set to `1`. - add_filter( 'pre_option_classifai_computer_vision', function() use( $options ) { + // Test with `descriptive_text_fields` set to `alt`. + add_filter( 'pre_option_classifai_feature_descriptive_text_generator', function() use( $options ) { return $options; } ); - $image_captions_settings = $this->get_computer_vision()->get_alt_text_settings(); + $image_captions_settings = ( new \Classifai\Features\DescriptiveTextGenerator() )->get_alt_text_settings(); $this->assertSame( $image_captions_settings, - array( - 'alt' => 'alt', - 'caption' => 0, - 'description' => 0, - ) + array( 'alt' ) ); // Test with `enable_image_captions` set to `no`. - $options['enable_image_captions'] = 'no'; - add_filter( 'pre_option_classifai_computer_vision', function() use( $options ) { + $options['descriptive_text_fields']['alt'] = '0'; + add_filter( 'pre_option_classifai_feature_descriptive_text_generator', function() use( $options ) { return $options; } ); - $image_captions_settings = $this->get_computer_vision()->get_alt_text_settings(); + $image_captions_settings = ( new \Classifai\Features\DescriptiveTextGenerator() )->get_alt_text_settings(); $this->assertSame( $image_captions_settings, - array( - 'alt' => 0, - 'caption' => 0, - 'description' => 0, - ) + array() ); } } diff --git a/tests/Classifai/Providers/Azure/SmartCroppingTest.php b/tests/Classifai/Providers/Azure/SmartCroppingTest.php index 23d241581..c9ae44a9a 100644 --- a/tests/Classifai/Providers/Azure/SmartCroppingTest.php +++ b/tests/Classifai/Providers/Azure/SmartCroppingTest.php @@ -36,7 +36,7 @@ public function tear_down() { * @return SmartCropping */ public function get_smart_cropping( - array $args = [ 'url' => 'my-api-url.com', 'api_key' => 'my-key' ] + array $args = [ 'endpoint_url' => 'my-api-url.com', 'api_key' => 'my-key' ] ) : SmartCropping { return new SmartCropping( $args ); } @@ -76,7 +76,7 @@ public function with_http_request_filter( callable $callback ) { public function test_get_wp_filesystem() { $this->assertInstanceOf( WP_Filesystem_Direct::class, - $this->get_smart_cropping()->get_wp_filesystem() + ( new \Classifai\Features\ImageCropping() )->get_wp_filesystem() ); } @@ -109,22 +109,22 @@ public function test_generate_attachment_metadata() { // Test that nothing happens when the metadata contains no sizes entry. $this->assertEquals( - [ 'no-sizes' => 1 ], - $this->get_smart_cropping()->generate_attachment_metadata( + [], + $this->get_smart_cropping()->generate_cropped_images( [ 'no-sizes' => 1 ], $attachment ) ); $with_filter_cb = function() use ( $attachment ) { - $filtered_data = $this->get_smart_cropping()->generate_attachment_metadata( + $filtered_data = $this->get_smart_cropping()->generate_cropped_images( wp_get_attachment_metadata( $attachment ), $attachment ); $this->assertEquals( - '33772-150x150.jpg', - $filtered_data['sizes']['thumbnail']['file'] + 150, + $filtered_data['thumbnail']['width'] ); }; @@ -144,8 +144,8 @@ public function test_get_cropped_thumbnail() { $this->assertWPError( $this->get_smart_cropping( [ - 'url' => 'my-bad-url.com', - 'api_key' => 'my-key', + 'endpoint_url' => 'my-bad-url.com', + 'api_key' => 'my-key', ] )->get_cropped_thumbnail( $attachment, @@ -155,29 +155,29 @@ public function test_get_cropped_thumbnail() { $with_filter_cb = function() use ( $attachment ) { - // Get the uploaded image url - $cropped_thumbnail_url = $this->get_smart_cropping()->get_cropped_thumbnail( + // Get the uploaded image data. + $cropped_thumbnail_data = $this->get_smart_cropping()->get_cropped_thumbnail( $attachment, - wp_get_attachment_metadata( $attachment )['sizes']['thumbnail'] + wp_get_attachment_metadata( $attachment )['sizes']['thumbnail'], ); - // Strip out everything before /wp-content/ because it won't match. - $prepped_url = substr( $cropped_thumbnail_url, strpos( $cropped_thumbnail_url , '/wp-content/' ) ); + $cropped_images['thumbnail'] = [ + 'width' => 150, + 'height' => 150, + 'data' => $cropped_thumbnail_data, + ]; + + $meta = ( new \Classifai\Features\ImageCropping() )->save( $cropped_images, $attachment ); $this->assertEquals( - sprintf( '%s/33772-150x150.jpg', wp_upload_dir()['path'] ), - $cropped_thumbnail_url + file_get_contents( DIR_TESTDATA .'/images/33772.jpg' ), + $cropped_thumbnail_data ); - // Test when file operations fail. - add_filter( 'classifai_smart_crop_wp_filesystem', '__return_false' ); - $this->assertWPError( - $this->get_smart_cropping()->get_cropped_thumbnail( - $attachment, - wp_get_attachment_metadata( $attachment )['sizes']['thumbnail'] - ) + $this->assertEquals( + '33772-150x150.jpg', + $meta['sizes']['thumbnail']['file'] ); - remove_filter( 'classifai_smart_crop_wp_filesystem', '__return_false' ); }; $this->with_http_request_filter( $with_filter_cb ); @@ -214,8 +214,8 @@ public function test_request_cropped_thumbnail() { $this->assertWPError( $this->get_smart_cropping( [ - 'url' => 'my-bad-url.com', - 'api_key' => 'my-key', + 'endpoint_url' => 'my-bad-url.com', + 'api_key' => 'my-key', ] )->request_cropped_thumbnail( [ diff --git a/tests/Classifai/Taxonomy/AbstractTaxonomyTest.php b/tests/Classifai/Taxonomy/AbstractTaxonomyTest.php index 571b0f00b..1fff4f472 100644 --- a/tests/Classifai/Taxonomy/AbstractTaxonomyTest.php +++ b/tests/Classifai/Taxonomy/AbstractTaxonomyTest.php @@ -54,19 +54,19 @@ class ThingTaxonomy extends AbstractTaxonomy { public $name = 'thing'; - public function get_name() { + public function get_name(): string { return $this->name; } - public function get_singular_label() { + public function get_singular_label(): string { return $this->name; } - public function get_plural_label() { + public function get_plural_label(): string { return $this->name . 's'; } - public function get_visibility() { + public function get_visibility(): bool { return true; } diff --git a/tests/Classifai/Watson/APIRequestTest.php b/tests/Classifai/Watson/APIRequestTest.php index 85b1f6eb7..b5314eeb2 100644 --- a/tests/Classifai/Watson/APIRequestTest.php +++ b/tests/Classifai/Watson/APIRequestTest.php @@ -1,6 +1,6 @@ [ 'watson_username' => 'foo-option' ] ] ); + update_option( 'classifai_feature_classification', [ 'ibm_watson_nlu' => [ 'username' => 'foo-option' ] ] ); $actual = $this->request->get_username(); $this->assertEquals( 'foo-option', $actual ); } @@ -49,7 +49,7 @@ function test_it_constant_password_if_present() { } function test_it_uses_option_password_if_present() { - update_option( 'classifai_watson_nlu', [ 'credentials' => [ 'watson_password' => 'foo-option' ] ] ); + update_option( 'classifai_feature_classification', [ 'ibm_watson_nlu' => [ 'password' => 'foo-option' ] ] ); $actual = $this->request->get_password(); $this->assertEquals( 'foo-option', $actual ); } diff --git a/tests/Classifai/Watson/ClassifierTest.php b/tests/Classifai/Watson/ClassifierTest.php index c453f71dd..b8fb7a9bb 100644 --- a/tests/Classifai/Watson/ClassifierTest.php +++ b/tests/Classifai/Watson/ClassifierTest.php @@ -1,6 +1,6 @@ classifier->get_request(); $this->assertInstanceOf( - '\Classifai\Watson\APIRequest', + '\Classifai\Providers\Watson\APIRequest', $actual ); } diff --git a/tests/Classifai/Watson/LinkerTest.php b/tests/Classifai/Watson/LinkerTest.php index 4c3a168eb..aa4c27357 100644 --- a/tests/Classifai/Watson/LinkerTest.php +++ b/tests/Classifai/Watson/LinkerTest.php @@ -1,6 +1,6 @@ settings ); - $this->provider = new NLU( 'service_name' ); + $this->provider = new NLU( new Classification() ); } /** @@ -55,29 +55,43 @@ public function test_retrieving_options() { /** * Tests the function providing debug information. */ - public function test_get_provider_debug_information() { + public function test_get_debug_information() { $this->assertEquals( [ - 'Configured', - 'API URL', - 'API username', - 'Post types', - 'Features', + 'Category (status)', + 'Category (threshold)', + 'Category (taxonomy)', + 'Keyword (status)', + 'Keyword (threshold)', + 'Keyword (taxonomy)', + 'Entity (status)', + 'Entity (threshold)', + 'Entity (taxonomy)', + 'Concept (status)', + 'Concept (threshold)', + 'Concept (taxonomy)', 'Latest response', ], - array_keys( $this->provider->get_provider_debug_information() ) + array_keys( $this->provider->get_debug_information() ) ); $this->assertEquals( [ - 'Configured' => 'yes', - 'API URL' => 'my-watson-url.com', - 'API username' => 'my-watson-username', - 'Post types' => 'post, attachment, event', - 'Features' => '{"feature":true}', + 'Category (status)' => 'Enabled', + 'Category (threshold)' => 'Enabled', + 'Category (taxonomy)' => 'Enabled', + 'Keyword (status)' => 'Enabled', + 'Keyword (threshold)' => 'Enabled', + 'Keyword (taxonomy)' => 'Enabled', + 'Entity (status)' => 'Disabled', + 'Entity (threshold)' => 'Enabled', + 'Entity (taxonomy)' => 'Enabled', + 'Concept (status)' => 'Disabled', + 'Concept (threshold)' => 'Enabled', + 'Concept (taxonomy)' => 'Enabled', 'Latest response' => 'N/A', ], - $this->provider->get_provider_debug_information( + $this->provider->get_debug_information( [ 'credentials' => [ 'watson_url' => 'my-watson-url.com', diff --git a/tests/Classifai/Watson/NormalizerTest.php b/tests/Classifai/Watson/NormalizerTest.php index fb335f154..6b3694262 100644 --- a/tests/Classifai/Watson/NormalizerTest.php +++ b/tests/Classifai/Watson/NormalizerTest.php @@ -1,6 +1,6 @@ { + beforeEach( () => { + cy.login(); + } ); + + const features = { + feature_classification: 'Classification', + feature_title_generation: 'Title Generation', + feature_excerpt_generation: 'Excerpt Generation', + feature_content_resizing: 'Content Resizing', + feature_text_to_speech_generation: 'Text to Speech', + feature_audio_transcripts_generation: 'Audio Transcripts Generation', + feature_image_generation: 'Image Generation', + feature_descriptive_text_generator: 'Descriptive Text Generator', + feature_image_tags_generator: 'Image Tags Generator', + feature_image_cropping: 'Image Cropping', + feature_image_to_text_generator: 'Image Text Extraction', + feature_pdf_to_text_generation: 'PDF Text Extraction', + }; + + const allowedRoles = [ + 'administrator', + 'editor', + 'author', + 'contributor', + 'subscriber', + ]; + + Object.keys( features ).forEach( ( feature ) => { + it( `"${ features[ feature ] }" feature common fields`, () => { + cy.visit( + `/wp-admin/tools.php?page=classifai&tab=language_processing&feature=${ feature }` + ); + + cy.get( '#status' ).should( + 'have.attr', + 'name', + `classifai_${ feature }[status]` + ); + cy.get( '#role_based_access' ).should( + 'have.attr', + 'name', + `classifai_${ feature }[role_based_access]` + ); + cy.get( '#user_based_access' ).should( + 'have.attr', + 'name', + `classifai_${ feature }[user_based_access]` + ); + cy.get( '#user_based_opt_out' ).should( + 'have.attr', + 'name', + `classifai_${ feature }[user_based_opt_out]` + ); + cy.get( '#provider' ).should( + 'have.attr', + 'name', + `classifai_${ feature }[provider]` + ); + cy.get( '#role_based_access' ).check(); + + for ( const role of allowedRoles ) { + if ( + 'feature_image_generation' === feature && + ( 'contributor' === role || 'subscriber' === role ) + ) { + continue; + } + + const roleField = cy.get( + `#classifai_${ feature }_roles_${ role }` + ); + roleField.should( 'be.visible' ); + roleField.should( 'have.value', role ); + roleField.should( + 'have.attr', + 'name', + `classifai_${ feature }[roles][${ role }]` + ); + } + + cy.get( '#role_based_access' ).uncheck(); + cy.get( '.allowed_roles_row' ).should( 'not.be.visible' ); + + cy.get( '#user_based_access' ).check(); + cy.get( '.allowed_users_row' ).should( 'be.visible' ); + + cy.get( '#user_based_access' ).uncheck(); + cy.get( '.allowed_users_row' ).should( 'not.be.visible' ); + } ); + } ); +} ); diff --git a/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js b/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js index be4a2efc4..0e0f8eed9 100644 --- a/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js +++ b/tests/cypress/integration/image-processing/image-generation-openai-dalle.test.js @@ -2,9 +2,9 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=image_processing&provider=openai_dalle' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); - cy.get( '#enable_image_gen' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); cy.optInAllFeatures(); } ); @@ -15,15 +15,17 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { it( 'Can save OpenAI "Image Processing" settings', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=image_processing&provider=openai_dalle' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#enable_image_gen' ).check(); - cy.get( '#openai_dalle_image_generation_roles_administrator' ).check(); - cy.get( '#number' ).select( '2' ); - cy.get( '#size' ).select( '512x512' ); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_image_generation_roles_administrator' + ).check(); + cy.get( '#number_of_images' ).select( '2' ); + cy.get( '#image_size' ).select( '512x512' ); cy.get( '#submit' ).click(); } ); @@ -80,9 +82,9 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { it( 'Can enable/disable image generation feature', () => { // Disable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=image_processing&provider=openai_dalle' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); - cy.get( '#enable_image_gen' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -90,9 +92,9 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=image_processing&provider=openai_dalle' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); - cy.get( '#enable_image_gen' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -101,11 +103,13 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { it( 'Can generate image directly in media library', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=image_processing&provider=openai_dalle' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); - cy.get( '#enable_image_gen' ).check(); - cy.get( '#openai_dalle_image_generation_roles_administrator' ).check(); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_image_generation_roles_administrator' + ).check(); cy.get( '#submit' ).click(); cy.visit( '/wp-admin/upload.php' ); @@ -128,27 +132,23 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { it( 'Can enable/disable image generation feature by role', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=image_processing&provider=openai_dalle' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_generation' ); - cy.get( '#enable_image_gen' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Disable admin role. - cy.disableFeatureForRoles( - 'image_generation', - [ 'administrator' ], - 'openai_dalle' - ); + cy.disableFeatureForRoles( 'feature_image_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyImageGenerationEnabled( false ); // Enable admin role. - cy.enableFeatureForRoles( - 'image_generation', - [ 'administrator' ], - 'openai_dalle' - ); + cy.enableFeatureForRoles( 'feature_image_generation', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyImageGenerationEnabled( true ); @@ -156,21 +156,15 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { it( 'Can enable/disable image generation feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'image_generation', - [ 'administrator' ], - 'openai_dalle' - ); + cy.disableFeatureForRoles( 'feature_image_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyImageGenerationEnabled( false ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'image_generation', - [ 'admin' ], - 'openai_dalle' - ); + cy.enableFeatureForUsers( 'feature_image_generation', [ 'admin' ] ); // Verify that the feature is available. cy.verifyImageGenerationEnabled( true ); @@ -178,16 +172,16 @@ describe( 'Image Generation (OpenAI DALL·E) Tests', () => { it( 'User can opt-out image generation feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'image_generation', 'openai_dalle' ); + cy.enableFeatureOptOut( 'feature_image_generation' ); // opt-out - cy.optOutFeature( 'image_generation' ); + cy.optOutFeature( 'feature_image_generation' ); // Verify that the feature is not available. cy.verifyImageGenerationEnabled( false ); // opt-in - cy.optInFeature( 'image_generation' ); + cy.optInFeature( 'feature_image_generation' ); // Verify that the feature is available. cy.verifyImageGenerationEnabled( true ); diff --git a/tests/cypress/integration/image-processing/image-processing-microsoft-azure.test.js b/tests/cypress/integration/image-processing/image-processing-microsoft-azure.test.js index 85de16ff2..1750703ac 100644 --- a/tests/cypress/integration/image-processing/image-processing-microsoft-azure.test.js +++ b/tests/cypress/integration/image-processing/image-processing-microsoft-azure.test.js @@ -6,13 +6,27 @@ describe( 'Image processing Tests', () => { before( () => { cy.login(); - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); - cy.get( '#computer_vision_enable_image_captions_alt' ).check(); - cy.get( '#computer_vision_enable_image_captions_description' ).check(); - cy.get( '#enable_image_tagging' ).check(); - cy.get( '#enable_smart_cropping' ).check(); - cy.get( '#enable_ocr' ).check(); - cy.get( '#submit' ).click(); + + const imageProcessingFeatures = [ + 'feature_descriptive_text_generator', + 'feature_image_tags_generator', + 'feature_image_cropping', + 'feature_image_to_text_generator', + 'feature_pdf_to_text_generation', + ]; + + imageProcessingFeatures.forEach( ( feature ) => { + cy.visit( + `/wp-admin/tools.php?page=classifai&tab=image_processing&feature=${ feature }` + ); + cy.get( '#status' ).check(); + cy.get( '#endpoint_url' ) + .clear() + .type( 'http://e2e-test-image-processing.test' ); + cy.get( '#api_key' ).clear().type( 'password' ); + cy.get( '#submit' ).click(); + } ); + cy.optInAllFeatures(); } ); @@ -20,24 +34,14 @@ describe( 'Image processing Tests', () => { cy.login(); } ); - it( 'Can save Azure AI Vision "Image Processing" settings', () => { - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); - - cy.get( '#url' ) - .clear() - .type( 'http://e2e-test-image-processing.test' ); - cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#computer_vision_enable_image_captions_alt' ).check(); - cy.get( '#computer_vision_enable_image_captions_description' ).check(); - cy.get( '#enable_image_tagging' ).check(); - cy.get( '#enable_smart_cropping' ).check(); - cy.get( '#enable_ocr' ).check(); - cy.get( '#submit' ).click(); - - cy.get( '.notice' ).contains( 'Settings saved.' ); - } ); - it( 'Can see Azure AI Vision Image processing actions on edit media page and verify Generated data.', () => { + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_descriptive_text_generator' + ); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_alt' + ).check(); + cy.get( '#submit' ).click(); cy.visit( '/wp-admin/upload.php?mode=grid' ); // Ensure grid mode is enabled. cy.visit( '/wp-admin/media-new.php' ); cy.get( '#plupload-upload-ui' ).should( 'exist' ); @@ -56,20 +60,20 @@ describe( 'Image processing Tests', () => { } ); // Verify Metabox with Image processing actions. - cy.get( '.postbox-header h2, #attachment_meta_box h2' ) + cy.get( '.postbox-header h2, #classifai_image_processing h2' ) .first() .contains( 'ClassifAI Image Processing' ); cy.get( - '.misc-publishing-actions label[for=rescan-captions]' + '#classifai_image_processing label[for=rescan-captions]' ).contains( 'No descriptive text? Rescan image' ); - cy.get( '.misc-publishing-actions label[for=rescan-tags]' ).contains( + cy.get( '#classifai_image_processing label[for=rescan-tags]' ).contains( 'Rescan image for new tags' ); - cy.get( '.misc-publishing-actions label[for=rescan-ocr]' ).contains( + cy.get( '#classifai_image_processing label[for=rescan-ocr]' ).contains( 'Rescan for text' ); cy.get( - '.misc-publishing-actions label[for=rescan-smart-crop]' + '#classifai_image_processing label[for=rescan-smart-crop]' ).should( 'exist' ); // Verify generated Data. @@ -89,7 +93,7 @@ describe( 'Image processing Tests', () => { } ); } ); - it( 'Can see Azure AI Vision Image processing actions on media model', () => { + it( 'Can see Azure AI Vision Image processing actions on media modal', () => { const imageId = imageEditLink.split( 'post=' )[ 1 ]?.split( '&' )[ 0 ]; mediaModelLink = `wp-admin/upload.php?item=${ imageId }`; cy.visit( mediaModelLink ); @@ -109,26 +113,75 @@ describe( 'Image processing Tests', () => { }; // Disable features - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); - cy.get( '#computer_vision_enable_image_captions_alt' ).uncheck(); - cy.get( '#computer_vision_enable_image_captions_caption' ).uncheck(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_descriptive_text_generator' + ); + cy.wait( 1000 ); cy.get( - '#computer_vision_enable_image_captions_description' + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_alt' ).uncheck(); - cy.get( '#enable_image_tagging' ).uncheck(); - cy.get( '#enable_smart_cropping' ).uncheck(); - cy.get( '#enable_ocr' ).uncheck(); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_caption' + ).uncheck(); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_description' + ).uncheck(); + cy.get( '#submit' ).click(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_tags_generator' + ); + cy.get( '#status' ).uncheck(); + cy.get( '#submit' ).click(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_cropping' + ); + cy.get( '#status' ).uncheck(); + cy.get( '#submit' ).click(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_to_text_generator' + ); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. cy.verifyAIVisionEnabled( false, options ); // Enable features. - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); - cy.get( '#computer_vision_enable_image_captions_alt' ).check(); - cy.get( '#enable_image_tagging' ).check(); - cy.get( '#enable_smart_cropping' ).check(); - cy.get( '#enable_ocr' ).check(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_descriptive_text_generator' + ); + cy.wait( 1000 ); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_alt' + ).check(); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_caption' + ).check(); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_description' + ).check(); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_tags_generator' + ); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_cropping' + ); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_image_to_text_generator' + ); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -142,59 +195,46 @@ describe( 'Image processing Tests', () => { }; // Enable features. - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); - cy.get( '#computer_vision_enable_image_captions_alt' ).check(); - cy.get( '#enable_image_tagging' ).check(); - cy.get( '#enable_smart_cropping' ).check(); - cy.get( '#enable_ocr' ).check(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_descriptive_text_generator' + ); + cy.wait( 1000 ); + cy.get( + '#classifai_feature_descriptive_text_generator_descriptive_text_fields_alt' + ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Disable access to admin role. - cy.disableFeatureForRoles( - 'image_captions', - [ 'administrator' ], - 'computer_vision' - ); - cy.disableFeatureForRoles( - 'image_tagging', - [ 'administrator' ], - 'computer_vision' - ); - cy.disableFeatureForRoles( - 'smart_cropping', - [ 'administrator' ], - 'computer_vision' - ); - cy.disableFeatureForRoles( - 'ocr', - [ 'administrator' ], - 'computer_vision' - ); + cy.disableFeatureForRoles( 'feature_descriptive_text_generator', [ + 'administrator', + ] ); + cy.disableFeatureForRoles( 'feature_image_tags_generator', [ + 'administrator', + ] ); + cy.disableFeatureForRoles( 'feature_image_cropping', [ + 'administrator', + ] ); + cy.disableFeatureForRoles( 'feature_image_to_text_generator', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyAIVisionEnabled( false, options ); // Enable access to admin role. - cy.enableFeatureForRoles( - 'image_captions', - [ 'administrator' ], - 'computer_vision' - ); - cy.enableFeatureForRoles( - 'image_tagging', - [ 'administrator' ], - 'computer_vision' - ); - cy.enableFeatureForRoles( - 'smart_cropping', - [ 'administrator' ], - 'computer_vision' - ); - cy.enableFeatureForRoles( - 'ocr', - [ 'administrator' ], - 'computer_vision' - ); + cy.enableFeatureForRoles( 'feature_descriptive_text_generator', [ + 'administrator', + ] ); + cy.enableFeatureForRoles( 'feature_image_tags_generator', [ + 'administrator', + ] ); + cy.enableFeatureForRoles( 'feature_image_cropping', [ + 'administrator', + ] ); + cy.enableFeatureForRoles( 'feature_image_to_text_generator', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyAIVisionEnabled( true, options ); @@ -207,46 +247,30 @@ describe( 'Image processing Tests', () => { }; // Disable access to admin role. - cy.disableFeatureForRoles( - 'image_captions', - [ 'administrator' ], - 'computer_vision' - ); - cy.disableFeatureForRoles( - 'image_tagging', - [ 'administrator' ], - 'computer_vision' - ); - cy.disableFeatureForRoles( - 'smart_cropping', - [ 'administrator' ], - 'computer_vision' - ); - cy.disableFeatureForRoles( - 'ocr', - [ 'administrator' ], - 'computer_vision' - ); + cy.disableFeatureForRoles( 'feature_descriptive_text_generator', [ + 'administrator', + ] ); + cy.disableFeatureForRoles( 'feature_image_tags_generator', [ + 'administrator', + ] ); + cy.disableFeatureForRoles( 'feature_image_cropping', [ + 'administrator', + ] ); + cy.disableFeatureForRoles( 'feature_image_to_text_generator', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyAIVisionEnabled( false, options ); - cy.enableFeatureForUsers( - 'image_captions', - [ 'admin' ], - 'computer_vision' - ); - cy.enableFeatureForUsers( - 'image_tagging', - [ 'admin' ], - 'computer_vision' - ); - cy.enableFeatureForUsers( - 'smart_cropping', - [ 'admin' ], - 'computer_vision' - ); - cy.enableFeatureForUsers( 'ocr', [ 'admin' ], 'computer_vision' ); + cy.enableFeatureForUsers( 'feature_descriptive_text_generator', [ + 'admin', + ] ); + cy.enableFeatureForUsers( 'feature_image_tags_generator', [ 'admin' ] ); + cy.enableFeatureForUsers( 'feature_image_cropping', [ 'admin' ] ); + cy.enableFeatureForUsers( 'feature_image_to_text_generator', [ + 'admin', + ] ); // Verify that the feature is available. cy.verifyAIVisionEnabled( true, options ); @@ -259,25 +283,25 @@ describe( 'Image processing Tests', () => { }; // Enable user based opt-out. - cy.enableFeatureOptOut( 'image_captions', 'computer_vision' ); - cy.enableFeatureOptOut( 'image_tagging', 'computer_vision' ); - cy.enableFeatureOptOut( 'smart_cropping', 'computer_vision' ); - cy.enableFeatureOptOut( 'ocr', 'computer_vision' ); + cy.enableFeatureOptOut( 'feature_descriptive_text_generator' ); + cy.enableFeatureOptOut( 'feature_image_tags_generator' ); + cy.enableFeatureOptOut( 'feature_image_cropping' ); + cy.enableFeatureOptOut( 'feature_image_to_text_generator' ); // opt-out - cy.optOutFeature( 'image_captions' ); - cy.optOutFeature( 'image_tagging' ); - cy.optOutFeature( 'smart_cropping' ); - cy.optOutFeature( 'ocr' ); + cy.optOutFeature( 'feature_descriptive_text_generator' ); + cy.optOutFeature( 'feature_image_tags_generator' ); + cy.optOutFeature( 'feature_image_cropping' ); + cy.optOutFeature( 'feature_image_to_text_generator' ); // Verify that the feature is not available. cy.verifyAIVisionEnabled( false, options ); // opt-in - cy.optInFeature( 'image_captions' ); - cy.optInFeature( 'image_tagging' ); - cy.optInFeature( 'smart_cropping' ); - cy.optInFeature( 'ocr' ); + cy.optInFeature( 'feature_descriptive_text_generator' ); + cy.optInFeature( 'feature_image_tags_generator' ); + cy.optInFeature( 'feature_image_cropping' ); + cy.optInFeature( 'feature_image_to_text_generator' ); // Verify that the feature is available. cy.verifyAIVisionEnabled( true, options ); diff --git a/tests/cypress/integration/image-processing/pdf-read.test.js b/tests/cypress/integration/image-processing/pdf-read.test.js index 29bb2f10a..59c79437a 100644 --- a/tests/cypress/integration/image-processing/pdf-read.test.js +++ b/tests/cypress/integration/image-processing/pdf-read.test.js @@ -3,8 +3,10 @@ import { getPDFData } from '../../plugins/functions'; describe( 'PDF read Tests', () => { before( () => { cy.login(); - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); - cy.get( '#enable_read_pdf' ).check(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_pdf_to_text_generation' + ); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); cy.optInAllFeatures(); } ); @@ -15,13 +17,15 @@ describe( 'PDF read Tests', () => { let pdfEditLink = ''; it( 'Can save "PDF scanning" settings', () => { - cy.visit( '/wp-admin/tools.php?page=classifai&tab=image_processing' ); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_pdf_to_text_generation' + ); - cy.get( '#url' ) + cy.get( '#endpoint_url' ) .clear() .type( 'http://e2e-test-image-processing.test' ); cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#enable_read_pdf' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); @@ -59,9 +63,9 @@ describe( 'PDF read Tests', () => { it( 'Can enable/disable PDF scanning feature', () => { // Disable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=computer_vision' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_pdf_to_text_generation' ); - cy.get( '#enable_read_pdf' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -72,9 +76,9 @@ describe( 'PDF read Tests', () => { // Enable admin role. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=computer_vision' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_pdf_to_text_generation' ); - cy.get( '#enable_read_pdf' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -87,17 +91,15 @@ describe( 'PDF read Tests', () => { it( 'Can enable/disable PDF scanning feature by role', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=computer_vision' + '/wp-admin/tools.php?page=classifai&tab=image_processing&feature=feature_pdf_to_text_generation' ); - cy.get( '#enable_read_pdf' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Disable admin role. - cy.disableFeatureForRoles( - 'read_pdf', - [ 'administrator' ], - 'computer_vision' - ); + cy.disableFeatureForRoles( 'feature_pdf_to_text_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.visit( pdfEditLink ); @@ -106,11 +108,9 @@ describe( 'PDF read Tests', () => { ); // Enable admin role. - cy.enableFeatureForRoles( - 'read_pdf', - [ 'administrator' ], - 'computer_vision' - ); + cy.enableFeatureForRoles( 'feature_pdf_to_text_generation', [ + 'administrator', + ] ); // Verify that the feature is available. cy.visit( pdfEditLink ); @@ -121,11 +121,9 @@ describe( 'PDF read Tests', () => { it( 'Can enable/disable PDF scanning feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'read_pdf', - [ 'administrator' ], - 'computer_vision' - ); + cy.disableFeatureForRoles( 'feature_pdf_to_text_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.visit( pdfEditLink ); @@ -134,7 +132,9 @@ describe( 'PDF read Tests', () => { ); // Enable feature for admin user. - cy.enableFeatureForUsers( 'read_pdf', [ 'admin' ], 'computer_vision' ); + cy.enableFeatureForUsers( 'feature_pdf_to_text_generation', [ + 'admin', + ] ); // Verify that the feature is available. cy.visit( pdfEditLink ); @@ -145,10 +145,10 @@ describe( 'PDF read Tests', () => { it( 'User can opt-out PDF scanning feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'read_pdf', 'computer_vision' ); + cy.enableFeatureOptOut( 'feature_pdf_to_text_generation' ); // opt-out - cy.optOutFeature( 'read_pdf' ); + cy.optOutFeature( 'feature_pdf_to_text_generation' ); // Verify that the feature is not available. cy.visit( pdfEditLink ); @@ -157,7 +157,7 @@ describe( 'PDF read Tests', () => { ); // opt-in - cy.optInFeature( 'read_pdf' ); + cy.optInFeature( 'feature_pdf_to_text_generation' ); // Verify that the feature is available. cy.visit( pdfEditLink ); diff --git a/tests/cypress/integration/language-processing/classify-content-ibm-watson.test.js b/tests/cypress/integration/language-processing/classify-content-ibm-watson.test.js index 4bd8e9182..8094e05bd 100644 --- a/tests/cypress/integration/language-processing/classify-content-ibm-watson.test.js +++ b/tests/cypress/integration/language-processing/classify-content-ibm-watson.test.js @@ -2,13 +2,26 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#classifai-settings-post' ).check(); - cy.get( '#classifai-settings-publish' ).check(); - cy.get( '#classifai-settings-category' ).check(); - cy.get( '#watson_nlu_classification_method_recommended_terms' ).check(); - cy.get( '#classifai-settings-enable_content_classification' ).check(); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( '#endpoint_url' ) + .clear() + .type( 'http://e2e-test-nlu-server.test/' ); + cy.get( '#password' ).clear().type( 'password' ); + cy.get( '#classifai-waston-cred-toggle' ).click(); + cy.get( '#classifai_feature_classification_post_types_post' ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_publish' + ).check(); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( + '#classifai_feature_classification_classification_method_recommended_terms' + ).check(); + cy.wait( 1000 ); + cy.get( '#category' ).check(); cy.get( '#submit' ).click(); cy.optInAllFeatures(); cy.disableClassicEditor(); @@ -21,34 +34,31 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () it( 'Can save IBM Watson "Language Processing" settings', () => { // Disable content classification by openai. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#enable_classification' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); - cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' - ); + cy.get( '#status' ).check(); + cy.get( '#classifai_feature_classification_post_types_post' ).check(); + cy.get( '#classifai_feature_classification_post_types_page' ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_draft' + ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_pending' + ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_private' + ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_publish' + ).check(); - cy.get( '#classifai-settings-watson_url' ) - .clear() - .type( 'http://e2e-test-nlu-server.test/' ); - cy.get( '#classifai-settings-watson_password' ) - .clear() - .type( 'password' ); - - cy.get( '#classifai-settings-automatic_classification' ).check(); - cy.get( '#classifai-settings-post' ).check(); - cy.get( '#classifai-settings-page' ).check(); - cy.get( '#classifai-settings-draft' ).check(); - cy.get( '#classifai-settings-pending' ).check(); - cy.get( '#classifai-settings-private' ).check(); - cy.get( '#classifai-settings-publish' ).check(); - - cy.get( '#classifai-settings-category' ).check(); - cy.get( '#classifai-settings-keyword' ).check(); - cy.get( '#classifai-settings-entity' ).check(); - cy.get( '#classifai-settings-concept' ).check(); + cy.get( '#category' ).check(); + cy.get( '#keyword' ).check(); + cy.get( '#entity' ).check(); + cy.get( '#concept' ).check(); cy.get( '#submit' ).click(); } ); @@ -94,7 +104,10 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () '/wp-admin/tools.php?page=classifai&tab=language_processing' ); - cy.get( '#classifai-settings-manual_review' ).check(); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( + '#classifai_feature_classification_classification_mode_manual_review' + ).check(); cy.get( '#submit' ).click(); // Create Test Post @@ -177,10 +190,13 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () cy.deactivatePlugin( 'classic-editor' ); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#classifai-settings-automatic_classification' ).check(); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( + '#classifai_feature_classification_classification_mode_automatic_classification' + ).check(); cy.get( '#submit' ).click(); // Create Test Post @@ -251,21 +267,13 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () // Update Threshold to 75. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#classifai-settings-category_threshold' ) - .clear() - .type( threshold ); - cy.get( '#classifai-settings-keyword_threshold' ) - .clear() - .type( threshold ); - cy.get( '#classifai-settings-entity_threshold' ) - .clear() - .type( threshold ); - cy.get( '#classifai-settings-concept_threshold' ) - .clear() - .type( threshold ); + cy.get( '#category_threshold' ).clear().type( threshold ); + cy.get( '#keyword_threshold' ).clear().type( threshold ); + cy.get( '#entity_threshold' ).clear().type( threshold ); + cy.get( '#concept_threshold' ).clear().type( threshold ); cy.get( '#submit' ).click(); // Create Test Post @@ -303,22 +311,17 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () const threshold1 = 75; // Update classification method to "Add recommended terms" and threshold value. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#watson_nlu_classification_method_recommended_terms' ).check(); - cy.get( '#classifai-settings-category_threshold' ) - .clear() - .type( threshold1 ); - cy.get( '#classifai-settings-keyword_threshold' ) - .clear() - .type( threshold1 ); - cy.get( '#classifai-settings-entity_threshold' ) - .clear() - .type( threshold1 ); - cy.get( '#classifai-settings-concept_threshold' ) - .clear() - .type( threshold1 ); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( + '#classifai_feature_classification_classification_method_recommended_terms' + ).check(); + cy.get( '#category_threshold' ).clear().type( threshold1 ); + cy.get( '#keyword_threshold' ).clear().type( threshold1 ); + cy.get( '#entity_threshold' ).clear().type( threshold1 ); + cy.get( '#concept_threshold' ).clear().type( threshold1 ); cy.get( '#submit' ).click(); // Create Test Post @@ -349,22 +352,17 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () const threshold2 = 70; // Update classification method to "Only classify based on existing terms" and threshold value. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#watson_nlu_classification_method_existing_terms' ).check(); - cy.get( '#classifai-settings-category_threshold' ) - .clear() - .type( threshold2 ); - cy.get( '#classifai-settings-keyword_threshold' ) - .clear() - .type( threshold2 ); - cy.get( '#classifai-settings-entity_threshold' ) - .clear() - .type( threshold2 ); - cy.get( '#classifai-settings-concept_threshold' ) - .clear() - .type( threshold2 ); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( + '#classifai_feature_classification_classification_method_existing_terms' + ).check(); + cy.get( '#category_threshold' ).clear().type( threshold2 ); + cy.get( '#keyword_threshold' ).clear().type( threshold2 ); + cy.get( '#entity_threshold' ).clear().type( threshold2 ); + cy.get( '#concept_threshold' ).clear().type( threshold2 ); cy.get( '#submit' ).click(); // Create Test Post @@ -393,23 +391,32 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () // Update classification method back to "Add recommended terms". cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#watson_nlu_classification_method_recommended_terms' ).check(); + cy.get( + '#classifai_feature_classification_classification_method_recommended_terms' + ).check(); cy.get( '#submit' ).click(); } ); it( 'Can create post and tags get created by ClassifAI', () => { const threshold = 70; cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#watson_nlu_classification_method_recommended_terms' ).check(); + cy.get( '#provider' ).select( 'ibm_watson_nlu' ); + cy.get( + '#classifai_feature_classification_classification_method_recommended_terms' + ).check(); cy.get( '#classifai-settings-category_taxonomy' ).select( 'post_tag' ); cy.get( '#classifai-settings-keyword_taxonomy' ).select( 'post_tag' ); cy.get( '#classifai-settings-entity_taxonomy' ).select( 'post_tag' ); cy.get( '#classifai-settings-concept_taxonomy' ).select( 'post_tag' ); + cy.get( '#category_threshold' ).clear().type( threshold ); + cy.get( '#keyword_threshold' ).clear().type( threshold ); + cy.get( '#entity_threshold' ).clear().type( threshold ); + cy.get( '#concept_threshold' ).clear().type( threshold ); cy.get( '#submit' ).click(); // Create Test Post @@ -436,9 +443,9 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () it( 'Can enable/disable Natural Language Understanding features.', () => { // Disable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#classifai-settings-enable_content_classification' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -446,9 +453,9 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#classifai-settings-enable_content_classification' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -458,13 +465,11 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () it( 'Can limit Natural Language Understanding features by roles', () => { // Disable access to admin role. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); + cy.get( '#role_based_access' ).check(); cy.get( - '#classifai-settings-content_classification_role_based_access' - ).check(); - cy.get( - '#watson_nlu_content_classification_roles_administrator' + '#classifai_feature_classification_roles_administrator' ).uncheck(); cy.get( '#submit' ).click(); @@ -477,11 +482,9 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () cy.visit( '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' ); + cy.get( '#role_based_access' ).check(); cy.get( - '#classifai-settings-content_classification_role_based_access' - ).check(); - cy.get( - '#watson_nlu_content_classification_roles_administrator' + '#classifai_feature_classification_roles_administrator' ).check(); cy.get( '#submit' ).click(); @@ -496,12 +499,8 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () cy.visit( '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' ); - cy.get( - '#classifai-settings-content_classification_role_based_access' - ).uncheck(); - cy.get( - '#classifai-settings-content_classification_user_based_access' - ).uncheck(); + cy.get( '#role_based_access' ).uncheck(); + cy.get( '#user_based_access' ).uncheck(); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); @@ -512,27 +511,23 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () cy.visit( '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' ); - cy.get( - '#classifai-settings-content_classification_role_based_access' - ).uncheck(); - cy.get( - '#classifai-settings-content_classification_user_based_access' - ).check(); + cy.get( '#role_based_access' ).uncheck(); + cy.get( '#user_based_access' ).check(); cy.get( 'body' ).then( ( $body ) => { if ( $body.find( - '#content_classification_users-container .components-form-token-field__remove-token' + '.allowed_users_row .components-form-token-field__remove-token' ).length > 0 ) { cy.get( - '#content_classification_users-container .components-form-token-field__remove-token' + '.allowed_users_row .components-form-token-field__remove-token' ).click( { multiple: true, } ); } } ); cy.get( - '#content_classification_users-container input.components-form-token-field__input' + '.allowed_users_row input.components-form-token-field__input' ).type( 'admin' ); cy.wait( 1000 ); cy.get( @@ -548,12 +543,8 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () cy.visit( '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' ); - cy.get( - '#classifai-settings-content_classification_role_based_access' - ).check(); - cy.get( - '#classifai-settings-content_classification_user_based_access' - ).uncheck(); + cy.get( '#role_based_access' ).check(); + cy.get( '#user_based_access' ).uncheck(); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); @@ -564,27 +555,21 @@ describe( '[Language processing] Classify content (IBM Watson - NLU) Tests', () cy.visit( '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' ); - cy.get( - '#classifai-settings-content_classification_role_based_access' - ).check(); - cy.get( - '#classifai-settings-content_classification_user_based_access' - ).check(); - cy.get( - '#classifai-settings-content_classification_user_based_opt_out' - ).check(); + cy.get( '#role_based_access' ).check(); + cy.get( '#user_based_access' ).check(); + cy.get( '#user_based_opt_out' ).check(); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); // opt-out - cy.optOutFeature( 'content_classification' ); + cy.optOutFeature( 'feature_classification' ); // Verify that the feature is not available. cy.verifyClassifyContentEnabled( false ); // opt-in - cy.optInFeature( 'content_classification' ); + cy.optInFeature( 'feature_classification' ); // Verify that the feature is available. cy.verifyClassifyContentEnabled( true ); diff --git a/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js b/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js index 4434e10fc..7d267e71f 100644 --- a/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js +++ b/tests/cypress/integration/language-processing/classify-content-openapi-embeddings.test.js @@ -2,9 +2,10 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#enable_classification' ).check(); + cy.get( '#status' ).check(); + cy.get( '#provider' ).select( 'openai_embeddings' ); cy.get( '#submit' ).click(); cy.optInAllFeatures(); cy.disableClassicEditor(); @@ -16,28 +17,50 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { it( 'Can save OpenAI Embeddings "Language Processing" settings', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#enable_classification' ).check(); - cy.get( '#openai_embeddings_post_types_post' ).check(); - cy.get( '#openai_embeddings_post_statuses_publish' ).check(); - cy.get( '#openai_embeddings_taxonomies_category' ).check(); - cy.get( '#openai_embeddings_taxonomies_category_threshold' ).type( 80 ); // "Test" requires 80% confidence. At 81%, it does not apply. - cy.get( '#number' ).clear().type( 1 ); + cy.get( '#status' ).check(); + cy.get( '#classifai_feature_classification_post_types_post' ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_publish' + ).check(); + cy.get( + '#classifai_feature_classification_openai_embeddings_taxonomies_category' + ).check(); + cy.get( + '#classifai_feature_classification_openai_embeddings_taxonomies_category_threshold' + ) + .clear() + .type( 100 ); // "Test" requires 80% confidence. At 81%, it does not apply. + cy.get( '#number_of_terms' ).clear().type( 1 ); cy.get( '#submit' ).click(); } ); + it( 'Can see the preview on the settings page', () => { + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' + ); + + cy.get( '#submit' ).click(); + + // Click the Preview button. + const closePanelSelector = '#get-classifier-preview-data-btn'; + cy.get( closePanelSelector ).click(); + + // Check the term is received and visible. + cy.get( '.tax-row--Category' ).should( 'exist' ); + } ); + it( 'Can create category and post and category will get auto-assigned', () => { // Remove custom taxonomies so those don't interfere with the test. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' + '/wp-admin/tools.php?page=classifai&tab=language_processing' ); - cy.get( '#classifai-settings-category' ).uncheck(); - cy.get( '#classifai-settings-keyword' ).uncheck(); - cy.get( '#classifai-settings-entity' ).uncheck(); - cy.get( '#classifai-settings-concept' ).uncheck(); + cy.get( + '#classifai_feature_classification_openai_embeddings_taxonomies_category' + ).uncheck(); cy.get( '#submit' ).click(); // Create test term. @@ -80,31 +103,17 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { ) .should( 'be.checked' ); cy.wrap( $panel ) - .find( - '.editor-post-taxonomies__hierarchical-terms-list .editor-post-taxonomies__hierarchical-terms-choice:first label' - ) + .find( '.editor-post-taxonomies__hierarchical-terms-list' ) + .children() .contains( 'Test' ); } ); } ); - it( 'Can see the preview on the settings page', () => { - cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' - ); - - // Click the Preview button. - const closePanelSelector = '#get-classifier-preview-data-btn'; - cy.get( closePanelSelector ).click(); - - // Check the term is received and visible. - cy.get( '.tax-row--Category' ).should( 'exist' ); - } ); - it( 'Can create category and post and category will not get auto-assigned if feature turned off', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#enable_classification' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Create test term. @@ -158,17 +167,21 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { cy.enableClassicEditor(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#enable_classification' ).check(); - cy.get( '#openai_embeddings_post_types_post' ).check(); - cy.get( '#openai_embeddings_post_statuses_publish' ).check(); - cy.get( '#openai_embeddings_taxonomies_category' ).check(); - cy.get( '#number' ).clear().type( 1 ); + cy.get( '#status' ).check(); + cy.get( '#classifai_feature_classification_post_types_post' ).check(); + cy.get( + '#classifai_feature_classification_post_statuses_publish' + ).check(); + cy.get( + '#classifai_feature_classification_openai_embeddings_taxonomies_category' + ).check(); + cy.get( '#number_of_terms' ).clear().type( 1 ); cy.get( '#submit' ).click(); - cy.classicCreatePost( { + cy.createClassicPost( { title: 'Embeddings test classic', content: "This feature uses OpenAI's Embeddings capabilities.", postType: 'post', @@ -181,11 +194,13 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { } ); it( 'Can enable/disable content classification feature ', () => { + cy.disableClassicEditor(); + // Disable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#enable_classification' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -193,9 +208,9 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_embeddings' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_classification' ); - cy.get( '#enable_classification' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -205,30 +220,26 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { it( 'Can enable/disable content classification feature by role', () => { // Remove custom taxonomies so those don't interfere with the test. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=watson_nlu' + '/wp-admin/tools.php?page=classifai&tab=language_processing' ); - cy.get( '#classifai-settings-category' ).uncheck(); - cy.get( '#classifai-settings-keyword' ).uncheck(); - cy.get( '#classifai-settings-entity' ).uncheck(); - cy.get( '#classifai-settings-concept' ).uncheck(); + + // Disable user-based access. + cy.get( '#user_based_access' ).uncheck(); + cy.get( '#submit' ).click(); // Disable admin role. - cy.disableFeatureForRoles( - 'classification', - [ 'administrator' ], - 'openai_embeddings' - ); + cy.disableFeatureForRoles( 'feature_classification', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyClassifyContentEnabled( false ); // Enable admin role. - cy.enableFeatureForRoles( - 'classification', - [ 'administrator' ], - 'openai_embeddings' - ); + cy.enableFeatureForRoles( 'feature_classification', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyClassifyContentEnabled( true ); @@ -236,21 +247,15 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { it( 'Can enable/disable content classification feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'classification', - [ 'administrator' ], - 'openai_embeddings' - ); + cy.disableFeatureForRoles( 'feature_classification', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyClassifyContentEnabled( false ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'classification', - [ 'admin' ], - 'openai_embeddings' - ); + cy.enableFeatureForUsers( 'feature_classification', [ 'admin' ] ); // Verify that the feature is available. cy.verifyClassifyContentEnabled( true ); @@ -258,16 +263,16 @@ describe( '[Language processing] Classify Content (OpenAI) Tests', () => { it( 'User can opt-out content classification feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'classification', 'openai_embeddings' ); + cy.enableFeatureOptOut( 'feature_classification', 'openai_embeddings' ); // opt-out - cy.optOutFeature( 'classification' ); + cy.optOutFeature( 'feature_classification' ); // Verify that the feature is not available. cy.verifyClassifyContentEnabled( false ); // opt-in - cy.optInFeature( 'classification' ); + cy.optInFeature( 'feature_classification' ); // Verify that the feature is available. cy.verifyClassifyContentEnabled( true ); diff --git a/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js b/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js index ce1aa2d88..0e02fb5c0 100644 --- a/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js +++ b/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js @@ -4,9 +4,10 @@ describe( '[Language processing] Excerpt Generation Tests', () => { before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); - cy.get( '#enable_excerpt' ).check(); + cy.get( '#status' ).check(); + cy.get( '#classifai_feature_excerpt_generation_post_types_post' ).check(); cy.get( '#submit' ).click(); cy.optInAllFeatures(); cy.disableClassicEditor(); @@ -18,15 +19,15 @@ describe( '[Language processing] Excerpt Generation Tests', () => { it( 'Can save OpenAI ChatGPT "Language Processing" settings', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#enable_excerpt' ).check(); - cy.get( '#excerpt_generation_role_based_access' ).check(); + cy.get( '#status' ).check(); + cy.get( '#role_based_access' ).check(); cy.get( - '#openai_chatgpt_excerpt_generation_roles_administrator' + '#classifai_feature_excerpt_generation_roles_administrator' ).check(); cy.get( '#length' ).clear().type( 35 ); cy.get( '#submit' ).click(); @@ -82,14 +83,14 @@ describe( '[Language processing] Excerpt Generation Tests', () => { cy.enableClassicEditor(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); - cy.get( '#enable_excerpt' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); const data = getChatGPTData(); - cy.classicCreatePost( { + cy.createClassicPost( { title: 'Excerpt test classic', content: 'Test GPT content.', postType: 'post', @@ -111,13 +112,14 @@ describe( '[Language processing] Excerpt Generation Tests', () => { it( 'Can set multiple custom excerpt generation prompts, select one as the default and delete one.', () => { cy.disableClassicEditor(); + cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); // Add three custom prompts. cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][0][default]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][0][default]"]' ) .parents( 'td:first' ) .find( 'button.js-classifai-add-prompt-fieldset' ) @@ -125,7 +127,7 @@ describe( '[Language processing] Excerpt Generation Tests', () => { .click() .click(); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][0][default]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][0][default]"]' ) .parents( 'td' ) .find( '.classifai-field-type-prompt-setting' ) @@ -133,40 +135,40 @@ describe( '[Language processing] Excerpt Generation Tests', () => { // Set the data for each prompt. cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][1][title]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][1][title]"]' ) .clear() .type( 'First custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][1][prompt]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][1][prompt]"]' ) .clear() .type( 'This is our first custom excerpt prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][2][title]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][2][title]"]' ) .clear() .type( 'Second custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][2][prompt]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][2][prompt]"]' ) .clear() .type( 'This prompt should be deleted' ); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][3][title]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][3][title]"]' ) .clear() .type( 'Third custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][3][prompt]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][3][prompt]"]' ) .clear() .type( 'This is a custom excerpt prompt' ); // Set the third prompt as our default. cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][3][default]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][3][default]"]' ) .parent() .find( 'a.action__set_default' ) @@ -174,7 +176,7 @@ describe( '[Language processing] Excerpt Generation Tests', () => { // Delete the second prompt. cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][2][default]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][2][default]"]' ) .parent() .find( 'a.action__remove_prompt' ) @@ -183,7 +185,7 @@ describe( '[Language processing] Excerpt Generation Tests', () => { .find( '.button-primary' ) .click(); cy.get( - '[name="classifai_openai_chatgpt[generate_excerpt_prompt][0][default]"]' + '[name="classifai_feature_excerpt_generation[generate_excerpt_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) @@ -236,9 +238,9 @@ describe( '[Language processing] Excerpt Generation Tests', () => { it( 'Can enable/disable excerpt generation feature', () => { // Disable features. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); - cy.get( '#enable_excerpt' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -246,9 +248,9 @@ describe( '[Language processing] Excerpt Generation Tests', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); - cy.get( '#enable_excerpt' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -257,27 +259,23 @@ describe( '[Language processing] Excerpt Generation Tests', () => { it( 'Can enable/disable excerpt generation feature by role', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); - cy.get( '#enable_excerpt' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Disable admin role. - cy.disableFeatureForRoles( - 'excerpt_generation', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.disableFeatureForRoles( 'feature_excerpt_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyExcerptGenerationEnabled( false ); // enable admin role. - cy.enableFeatureForRoles( - 'excerpt_generation', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.enableFeatureForRoles( 'feature_excerpt_generation', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyExcerptGenerationEnabled( true ); @@ -285,21 +283,17 @@ describe( '[Language processing] Excerpt Generation Tests', () => { it( 'Can enable/disable excerpt generation feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'excerpt_generation', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.disableFeatureForRoles( 'feature_excerpt_generation', [ + 'administrator', + ] ); + + cy.enableFeatureForUsers( 'feature_excerpt_generation', [] ); // Verify that the feature is not available. cy.verifyExcerptGenerationEnabled( false ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'excerpt_generation', - [ 'admin' ], - 'openai_chatgpt' - ); + cy.enableFeatureForUsers( 'feature_excerpt_generation', [ 'admin' ] ); // Verify that the feature is available. cy.verifyExcerptGenerationEnabled( true ); @@ -307,16 +301,19 @@ describe( '[Language processing] Excerpt Generation Tests', () => { it( 'User can opt-out excerpt generation feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'excerpt_generation', 'openai_chatgpt' ); + cy.enableFeatureOptOut( + 'feature_excerpt_generation', + 'openai_chatgpt' + ); // opt-out - cy.optOutFeature( 'excerpt_generation' ); + cy.optOutFeature( 'feature_excerpt_generation' ); // Verify that the feature is not available. cy.verifyExcerptGenerationEnabled( false ); // opt-in - cy.optInFeature( 'excerpt_generation' ); + cy.optInFeature( 'feature_excerpt_generation' ); // Verify that the feature is available. cy.verifyExcerptGenerationEnabled( true ); diff --git a/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js b/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js index e79250670..9d4fbb0c9 100644 --- a/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js +++ b/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js @@ -2,9 +2,10 @@ describe( '[Language processing] Speech to Text Tests', () => { before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' ); - cy.get( '#enable_resize_content' ).check(); + cy.get( '#status' ).check(); + cy.get( '#api_key' ).type( 'abc123' ); cy.get( '#submit' ).click(); cy.optInAllFeatures(); cy.disableClassicEditor(); @@ -16,11 +17,13 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Resize content feature can grow and shrink content', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' ); - cy.get( '#enable_resize_content' ).check(); - cy.get( '#openai_chatgpt_resize_content_roles_administrator' ).check(); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_content_resizing_roles_administrator' + ).check(); cy.get( '#submit' ).click(); cy.createPost( { @@ -73,12 +76,12 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Can set multiple custom resize generation prompts, select one as the default and delete one.', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' ); // Add three custom shrink prompts. cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][0][default]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][0][default]"]' ) .parents( 'td:first' ) .find( 'button.js-classifai-add-prompt-fieldset' ) @@ -86,7 +89,7 @@ describe( '[Language processing] Speech to Text Tests', () => { .click() .click(); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][0][default]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) @@ -94,7 +97,7 @@ describe( '[Language processing] Speech to Text Tests', () => { // Add three custom grow prompts. cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][0][default]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][0][default]"]' ) .parents( 'td:first' ) .find( 'button.js-classifai-add-prompt-fieldset:first' ) @@ -102,7 +105,7 @@ describe( '[Language processing] Speech to Text Tests', () => { .click() .click(); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][0][default]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) @@ -110,77 +113,77 @@ describe( '[Language processing] Speech to Text Tests', () => { // Set the data for each prompt. cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][1][title]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][1][title]"]' ) .clear() .type( 'First custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][1][prompt]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][1][prompt]"]' ) .clear() .type( 'This is our first custom shrink prompt' ); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][2][title]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][2][title]"]' ) .clear() .type( 'Second custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][2][prompt]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][2][prompt]"]' ) .clear() .type( 'This prompt should be deleted' ); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][3][title]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][3][title]"]' ) .clear() .type( 'Third custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][3][prompt]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][3][prompt]"]' ) .clear() .type( 'This is a custom shrink prompt' ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][1][title]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][1][title]"]' ) .clear() .type( 'First custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][1][prompt]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][1][prompt]"]' ) .clear() .type( 'This is our first custom grow prompt' ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][2][title]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][2][title]"]' ) .clear() .type( 'Second custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][2][prompt]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][2][prompt]"]' ) .clear() .type( 'This prompt should be deleted' ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][3][title]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][3][title]"]' ) .clear() .type( 'Third custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][3][prompt]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][3][prompt]"]' ) .clear() .type( 'This is a custom grow prompt' ); // Set the third prompt as our default. cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][3][default]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][3][default]"]' ) .parent() .find( 'a.action__set_default' ) .click( { force: true } ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][3][default]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][3][default]"]' ) .parent() .find( 'a.action__set_default' ) @@ -188,7 +191,7 @@ describe( '[Language processing] Speech to Text Tests', () => { // Delete the second prompt. cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][2][default]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][2][default]"]' ) .parent() .find( 'a.action__remove_prompt' ) @@ -197,22 +200,23 @@ describe( '[Language processing] Speech to Text Tests', () => { .find( '.button-primary' ) .click(); cy.get( - '[name="classifai_openai_chatgpt[shrink_content_prompt][0][default]"]' + '[name="classifai_feature_content_resizing[condense_text_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) .should( 'have.length', 3 ); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][2][default]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][2][default]"]' ) .parent() .find( 'a.action__remove_prompt' ) .click( { force: true } ); cy.get( 'div[aria-describedby="js-classifai--delete-prompt-modal"]' ) + .first() .find( '.button-primary' ) .click(); cy.get( - '[name="classifai_openai_chatgpt[grow_content_prompt][0][default]"]' + '[name="classifai_feature_content_resizing[expand_text_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) @@ -271,9 +275,9 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Can enable/disable resize content feature', () => { // Disable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' ); - cy.get( '#enable_resize_content' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -281,9 +285,9 @@ describe( '[Language processing] Speech to Text Tests', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' ); - cy.get( '#enable_resize_content' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -292,21 +296,17 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Can enable/disable resize content feature by role', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'resize_content', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.disableFeatureForRoles( 'feature_content_resizing', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyResizeContentEnabled( false ); // Enable admin role. - cy.enableFeatureForRoles( - 'resize_content', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.enableFeatureForRoles( 'feature_content_resizing', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyResizeContentEnabled( true ); @@ -314,21 +314,15 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Can enable/disable resize content feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'resize_content', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.disableFeatureForRoles( 'feature_content_resizing', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyResizeContentEnabled( false ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'resize_content', - [ 'admin' ], - 'openai_chatgpt' - ); + cy.enableFeatureForUsers( 'feature_content_resizing', [ 'admin' ] ); // Verify that the feature is available. cy.verifyResizeContentEnabled( true ); @@ -336,16 +330,16 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'User can opt-out resize content feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'resize_content', 'openai_chatgpt' ); + cy.enableFeatureOptOut( 'feature_content_resizing' ); // opt-out - cy.optOutFeature( 'resize_content' ); + cy.optOutFeature( 'feature_content_resizing' ); // Verify that the feature is not available. cy.verifyResizeContentEnabled( false ); // opt-in - cy.optInFeature( 'resize_content' ); + cy.optInFeature( 'feature_content_resizing' ); // Verify that the feature is available. cy.verifyResizeContentEnabled( true ); diff --git a/tests/cypress/integration/language-processing/speech-to-text-openapi-whisper.test.js b/tests/cypress/integration/language-processing/speech-to-text-openapi-whisper.test.js index fea371a00..9e04582d5 100644 --- a/tests/cypress/integration/language-processing/speech-to-text-openapi-whisper.test.js +++ b/tests/cypress/integration/language-processing/speech-to-text-openapi-whisper.test.js @@ -4,9 +4,9 @@ describe( '[Language processing] Speech to Text Tests', () => { before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_whisper' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_audio_transcripts_generation' ); - cy.get( '#enable_transcripts' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); cy.optInAllFeatures(); cy.disableClassicEditor(); @@ -18,13 +18,15 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Can save OpenAI Whisper "Language Processing" settings', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_whisper' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_audio_transcripts_generation' ); cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#enable_transcripts' ).check(); - cy.get( '#openai_whisper_speech_to_text_roles_administrator' ).check(); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_audio_transcripts_generation_roles_administrator' + ).check(); cy.get( '#submit' ).click(); } ); @@ -81,9 +83,9 @@ describe( '[Language processing] Speech to Text Tests', () => { // Disable features cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_whisper' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_audio_transcripts_generation' ); - cy.get( '#enable_transcripts' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -91,9 +93,9 @@ describe( '[Language processing] Speech to Text Tests', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_whisper' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_audio_transcripts_generation' ); - cy.get( '#enable_transcripts' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -103,9 +105,9 @@ describe( '[Language processing] Speech to Text Tests', () => { it( 'Can enable/disable speech to text feature by role', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_whisper' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_audio_transcripts_generation' ); - cy.get( '#enable_transcripts' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); const options = { @@ -114,21 +116,17 @@ describe( '[Language processing] Speech to Text Tests', () => { }; // Disable admin role. - cy.disableFeatureForRoles( - 'speech_to_text', - [ 'administrator' ], - 'openai_whisper' - ); + cy.disableFeatureForRoles( 'feature_audio_transcripts_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifySpeechToTextEnabled( false, options ); // Enable admin role. - cy.enableFeatureForRoles( - 'speech_to_text', - [ 'administrator' ], - 'openai_whisper' - ); + cy.enableFeatureForRoles( 'feature_audio_transcripts_generation', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifySpeechToTextEnabled( true, options ); @@ -141,21 +139,17 @@ describe( '[Language processing] Speech to Text Tests', () => { }; // Disable admin role. - cy.disableFeatureForRoles( - 'speech_to_text', - [ 'administrator' ], - 'openai_whisper' - ); + cy.disableFeatureForRoles( 'feature_audio_transcripts_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifySpeechToTextEnabled( false, options ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'speech_to_text', - [ 'admin' ], - 'openai_whisper' - ); + cy.enableFeatureForUsers( 'feature_audio_transcripts_generation', [ + 'admin', + ] ); // Verify that the feature is available. cy.verifySpeechToTextEnabled( true, options ); @@ -168,16 +162,16 @@ describe( '[Language processing] Speech to Text Tests', () => { }; // Enable user based opt-out. - cy.enableFeatureOptOut( 'speech_to_text', 'openai_whisper' ); + cy.enableFeatureOptOut( 'feature_audio_transcripts_generation' ); // opt-out - cy.optOutFeature( 'speech_to_text' ); + cy.optOutFeature( 'feature_audio_transcripts_generation' ); // Verify that the feature is not available. cy.verifySpeechToTextEnabled( false, options ); // opt-in - cy.optInFeature( 'speech_to_text' ); + cy.optInFeature( 'feature_audio_transcripts_generation' ); // Verify that the feature is available. cy.verifySpeechToTextEnabled( true, options ); diff --git a/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js b/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js index cfc4d24cd..17302c216 100644 --- a/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js +++ b/tests/cypress/integration/language-processing/text-to-speech-microsoft-azure.test.js @@ -2,13 +2,15 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => before( () => { cy.login(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=azure_text_to_speech' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_text_to_speech_generation' ); - cy.get( '#azure_text_to_speech_post_types_post' ).check( 'post' ); - cy.get( '#url' ).clear(); - cy.get( '#url' ).type( 'https://service.com' ); + cy.get( + '#classifai_feature_text_to_speech_generation_post_types_post' + ).check( 'post' ); + cy.get( '#endpoint_url' ).clear(); + cy.get( '#endpoint_url' ).type( 'https://service.com' ); cy.get( '#api_key' ).type( 'password' ); - cy.get( '#enable_text_to_speech' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); cy.get( '#voice' ).select( 'en-AU-AnnetteNeural|Female' ); @@ -91,7 +93,7 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => it( 'Can see the enable button in a post (Classic Editor)', () => { cy.enableClassicEditor(); - cy.classicCreatePost( { + cy.createClassicPost( { title: 'Text to Speech test classic', content: "This feature uses Microsoft's Text to Speech capabilities.", @@ -109,10 +111,14 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => } ); it( 'Disable support for post type Post', () => { + cy.disableClassicEditor(); + cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=azure_text_to_speech' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_text_to_speech_generation' ); - cy.get( '#azure_text_to_speech_post_types_post' ).uncheck( 'post' ); + cy.get( + '#classifai_feature_text_to_speech_generation_post_types_post' + ).uncheck( 'post' ); cy.get( '#submit' ).click(); cy.visit( '/text-to-speech-test/' ); @@ -122,9 +128,9 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => it( 'Can enable/disable text to speech feature', () => { // Disable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=azure_text_to_speech' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_text_to_speech_generation' ); - cy.get( '#enable_text_to_speech' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -132,10 +138,12 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=azure_text_to_speech' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_text_to_speech_generation' ); - cy.get( '#enable_text_to_speech' ).check(); - cy.get( '#azure_text_to_speech_post_types_post' ).check( 'post' ); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_text_to_speech_generation_post_types_post' + ).check( 'post' ); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -145,27 +153,25 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => it( 'Can enable/disable text to speech feature by role', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=azure_text_to_speech' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_text_to_speech_generation' ); - cy.get( '#azure_text_to_speech_post_types_post' ).check( 'post' ); + cy.get( + '#classifai_feature_text_to_speech_generation_post_types_post' + ).check( 'post' ); cy.get( '#submit' ).click(); // Disable admin role. - cy.disableFeatureForRoles( - 'text_to_speech', - [ 'administrator' ], - 'azure_text_to_speech' - ); + cy.disableFeatureForRoles( 'feature_text_to_speech_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyTextToSpeechEnabled( false ); // Enable admin role. - cy.enableFeatureForRoles( - 'text_to_speech', - [ 'administrator' ], - 'azure_text_to_speech' - ); + cy.enableFeatureForRoles( 'feature_text_to_speech_generation', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyTextToSpeechEnabled( true ); @@ -173,21 +179,17 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => it( 'Can enable/disable text to speech feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'text_to_speech', - [ 'administrator' ], - 'azure_text_to_speech' - ); + cy.disableFeatureForRoles( 'feature_text_to_speech_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyTextToSpeechEnabled( false ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'text_to_speech', - [ 'admin' ], - 'azure_text_to_speech' - ); + cy.enableFeatureForUsers( 'feature_text_to_speech_generation', [ + 'admin', + ] ); // Verify that the feature is available. cy.verifyTextToSpeechEnabled( true ); @@ -195,16 +197,16 @@ describe( '[Language Processing] Text to Speech (Microsoft Azure) Tests', () => it( 'User can opt-out text to speech feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'text_to_speech', 'azure_text_to_speech' ); + cy.enableFeatureOptOut( 'feature_text_to_speech_generation' ); // opt-out - cy.optOutFeature( 'text_to_speech' ); + cy.optOutFeature( 'feature_text_to_speech_generation' ); // Verify that the feature is not available. cy.verifyTextToSpeechEnabled( false ); // opt-in - cy.optInFeature( 'text_to_speech' ); + cy.optInFeature( 'feature_text_to_speech_generation' ); // Verify that the feature is available. cy.verifyTextToSpeechEnabled( true ); diff --git a/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js b/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js index 98f3f21bd..05ae7e787 100644 --- a/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js +++ b/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js @@ -13,15 +13,15 @@ describe( '[Language processing] Title Generation Tests', () => { it( 'Can save OpenAI ChatGPT "Language Processing" title settings', () => { cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); cy.get( '#api_key' ).clear().type( 'password' ); - cy.get( '#enable_titles' ).check(); + cy.get( '#status' ).check(); cy.get( - '#openai_chatgpt_title_generation_roles_administrator' + '#classifai_feature_title_generation_roles_administrator' ).check(); - cy.get( '#number_titles' ).select( 1 ); + cy.get( '#number_of_titles' ).type( 1 ); cy.get( '#submit' ).click(); } ); @@ -92,9 +92,9 @@ describe( '[Language processing] Title Generation Tests', () => { cy.enableClassicEditor(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); - cy.get( '#enable_titles' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); const data = getChatGPTData(); @@ -118,12 +118,12 @@ describe( '[Language processing] Title Generation Tests', () => { it( 'Can set multiple custom title generation prompts, select one as the default and delete one.', () => { cy.disableClassicEditor(); cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); // Add three custom prompts. cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][0][default]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][0][default]"]' ) .parents( 'td:first' ) .find( 'button.js-classifai-add-prompt-fieldset' ) @@ -131,7 +131,7 @@ describe( '[Language processing] Title Generation Tests', () => { .click() .click(); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][0][default]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) @@ -139,40 +139,40 @@ describe( '[Language processing] Title Generation Tests', () => { // Set the data for each prompt. cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][1][title]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][1][title]"]' ) .clear() .type( 'First custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][1][prompt]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][1][prompt]"]' ) .clear() .type( 'This is our first custom title prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][2][title]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][2][title]"]' ) .clear() .type( 'Second custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][2][prompt]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][2][prompt]"]' ) .clear() .type( 'This prompt should be deleted' ); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][3][title]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][3][title]"]' ) .clear() .type( 'Third custom prompt' ); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][3][prompt]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][3][prompt]"]' ) .clear() .type( 'This is a custom title prompt' ); // Set the third prompt as our default. cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][3][default]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][3][default]"]' ) .parent() .find( 'a.action__set_default' ) @@ -180,7 +180,7 @@ describe( '[Language processing] Title Generation Tests', () => { // Delete the second prompt. cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][2][default]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][2][default]"]' ) .parent() .find( 'a.action__remove_prompt' ) @@ -189,7 +189,7 @@ describe( '[Language processing] Title Generation Tests', () => { .find( '.button-primary' ) .click(); cy.get( - '[name="classifai_openai_chatgpt[generate_title_prompt][0][default]"]' + '[name="classifai_feature_title_generation[generate_title_prompt][0][default]"]' ) .parents( 'td:first' ) .find( '.classifai-field-type-prompt-setting' ) @@ -262,9 +262,9 @@ describe( '[Language processing] Title Generation Tests', () => { it( 'Can enable/disable title generation feature', () => { // Disable features. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); - cy.get( '#enable_titles' ).uncheck(); + cy.get( '#status' ).uncheck(); cy.get( '#submit' ).click(); // Verify that the feature is not available. @@ -272,9 +272,9 @@ describe( '[Language processing] Title Generation Tests', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); - cy.get( '#enable_titles' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Verify that the feature is available. @@ -284,27 +284,23 @@ describe( '[Language processing] Title Generation Tests', () => { it( 'Can enable/disable title generation feature by role', () => { // Enable feature. cy.visit( - '/wp-admin/tools.php?page=classifai&tab=language_processing&provider=openai_chatgpt' + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); - cy.get( '#enable_titles' ).check(); + cy.get( '#status' ).check(); cy.get( '#submit' ).click(); // Disable admin role. - cy.disableFeatureForRoles( - 'title_generation', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.disableFeatureForRoles( 'feature_title_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyTitleGenerationEnabled( false ); // Enable admin role. - cy.enableFeatureForRoles( - 'title_generation', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.enableFeatureForRoles( 'feature_title_generation', [ + 'administrator', + ] ); // Verify that the feature is available. cy.verifyTitleGenerationEnabled( true ); @@ -312,21 +308,15 @@ describe( '[Language processing] Title Generation Tests', () => { it( 'Can enable/disable title generation feature by user', () => { // Disable admin role. - cy.disableFeatureForRoles( - 'title_generation', - [ 'administrator' ], - 'openai_chatgpt' - ); + cy.disableFeatureForRoles( 'feature_title_generation', [ + 'administrator', + ] ); // Verify that the feature is not available. cy.verifyTitleGenerationEnabled( false ); // Enable feature for admin user. - cy.enableFeatureForUsers( - 'title_generation', - [ 'admin' ], - 'openai_chatgpt' - ); + cy.enableFeatureForUsers( 'feature_title_generation', [ 'admin' ] ); // Verify that the feature is available. cy.verifyTitleGenerationEnabled( true ); @@ -334,16 +324,16 @@ describe( '[Language processing] Title Generation Tests', () => { it( 'User can opt-out title generation feature', () => { // Enable user based opt-out. - cy.enableFeatureOptOut( 'title_generation', 'openai_chatgpt' ); + cy.enableFeatureOptOut( 'feature_title_generation' ); // opt-out - cy.optOutFeature( 'title_generation' ); + cy.optOutFeature( 'feature_title_generation' ); // Verify that the feature is not available. cy.verifyTitleGenerationEnabled( false ); // opt-in - cy.optInFeature( 'title_generation' ); + cy.optInFeature( 'feature_title_generation' ); // Verify that the feature is available. cy.verifyTitleGenerationEnabled( true ); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index facdfcbea..9c1513100 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -126,17 +126,16 @@ Cypress.Commands.add( 'optInAllFeatures', () => { /** * Enable role based access for a feature. * - * @param {string} feature The feature to enable. - * @param {string} roles The roles to enable. - * @param {string} provider The provider to enable. + * @param {string} feature The feature to enable. + * @param {string} roles The roles to enable. */ -Cypress.Commands.add( 'enableFeatureForRoles', ( feature, roles, provider ) => { +Cypress.Commands.add( 'enableFeatureForRoles', ( feature, roles ) => { cy.visit( - `/wp-admin/tools.php?page=classifai&tab=language_processing&provider=${ provider }` + `/wp-admin/tools.php?page=classifai&tab=language_processing&feature=${ feature }` ); - cy.get( `#${ feature }_role_based_access` ).check(); + cy.get( `#role_based_access` ).check(); roles.forEach( ( role ) => { - cy.get( `#${ provider }_${ feature }_roles_${ role }` ).check(); + cy.get( `#classifai_${ feature }_roles_${ role }` ).check(); } ); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); @@ -145,46 +144,40 @@ Cypress.Commands.add( 'enableFeatureForRoles', ( feature, roles, provider ) => { /** * Disable role based access for a feature. * - * @param {string} feature The feature to disable. - * @param {string} roles The roles to disable. - * @param {string} provider The provider to disable. + * @param {string} feature The feature to disable. + * @param {string} roles The roles to disable. */ -Cypress.Commands.add( - 'disableFeatureForRoles', - ( feature, roles, provider ) => { - cy.visit( - `/wp-admin/tools.php?page=classifai&tab=language_processing&provider=${ provider }` - ); - cy.get( `#${ feature }_role_based_access` ).check(); - roles.forEach( ( role ) => { - cy.get( `#${ provider }_${ feature }_roles_${ role }` ).uncheck(); - } ); - cy.get( '#submit' ).click(); - cy.get( '.notice' ).contains( 'Settings saved.' ); - } -); +Cypress.Commands.add( 'disableFeatureForRoles', ( feature, roles ) => { + cy.visit( + `/wp-admin/tools.php?page=classifai&tab=language_processing&feature=${ feature }` + ); + cy.get( '#status' ).check(); + cy.get( `#role_based_access` ).check(); + roles.forEach( ( role ) => { + cy.get( `#classifai_${ feature }_roles_${ role }` ).uncheck(); + } ); + cy.get( '#submit' ).click(); + cy.get( '.notice' ).contains( 'Settings saved.' ); +} ); /** * Enable user based access for a feature. * - * @param {string} feature The feature to enable. - * @param {string} users The users to enable. - * @param {string} provider The provider to enable. + * @param {string} feature The feature to enable. + * @param {string} users The users to enable. */ -Cypress.Commands.add( 'enableFeatureForUsers', ( feature, users, provider ) => { +Cypress.Commands.add( 'enableFeatureForUsers', ( feature, users ) => { cy.visit( - `/wp-admin/tools.php?page=classifai&tab=language_processing&provider=${ provider }` + `/wp-admin/tools.php?page=classifai&tab=language_processing&feature=${ feature }` ); - cy.get( `#${ feature }_user_based_access` ).check(); - cy.get( 'body' ).then( ( $body ) => { + cy.get( `#user_based_access` ).check(); + cy.wait( 1000 ); + cy.get( '.allowed_users_row' ).then( ( $body ) => { if ( - $body.find( - `#${ feature }_users-container .components-form-token-field__remove-token` - ).length > 0 + $body.find( `.components-form-token-field__remove-token` ).length > + 0 ) { - cy.get( - `#${ feature }_users-container .components-form-token-field__remove-token` - ).click( { + cy.get( `.components-form-token-field__remove-token` ).click( { multiple: true, } ); } @@ -192,12 +185,10 @@ Cypress.Commands.add( 'enableFeatureForUsers', ( feature, users, provider ) => { users.forEach( ( user ) => { cy.get( - `#${ feature }_users-container input.components-form-token-field__input` + `.allowed_users_row input.components-form-token-field__input` ).type( user ); - cy.wait( 1000 ); - cy.get( - 'ul.components-form-token-field__suggestions-list li:nth-child(1)' - ).click(); + + cy.get( '[aria-label="admin (admin)"]' ).click(); } ); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); @@ -206,17 +197,16 @@ Cypress.Commands.add( 'enableFeatureForUsers', ( feature, users, provider ) => { /** * Enable user based opt-out for a feature. * - * @param {string} feature The feature to enable. - * @param {string} provider The provider to enable. + * @param {string} feature The feature to enable. */ -Cypress.Commands.add( 'enableFeatureOptOut', ( feature, provider ) => { +Cypress.Commands.add( 'enableFeatureOptOut', ( feature ) => { cy.visit( - `/wp-admin/tools.php?page=classifai&tab=language_processing&provider=${ provider }` + `/wp-admin/tools.php?page=classifai&tab=language_processing&feature=${ feature }` ); - cy.get( `#${ feature }_role_based_access` ).check(); - cy.get( `#${ provider }_${ feature }_roles_administrator` ).check(); - cy.get( `#${ feature }_user_based_access` ).uncheck(); - cy.get( `#${ feature }_user_based_opt_out` ).check(); + cy.get( `#role_based_access` ).check(); + cy.get( `#classifai_${ feature }_roles_administrator` ).check(); + cy.get( `#user_based_access` ).uncheck(); + cy.get( `#user_based_opt_out` ).check(); cy.get( '#submit' ).click(); cy.get( '.notice' ).contains( 'Settings saved.' ); @@ -324,10 +314,15 @@ Cypress.Commands.add( */ Cypress.Commands.add( 'verifyTextToSpeechEnabled', ( enabled = true ) => { const shouldExist = enabled ? 'exist' : 'not.exist'; + cy.visit( '/wp-admin/edit.php' ); cy.get( '#the-list tr:nth-child(1) td.title a.row-title' ).click(); cy.closeWelcomeGuide(); - cy.get( '.classifai-panel' ).click(); + cy.get( 'body' ).then( ( $body ) => { + if ( $body.find( '.classifai-panel' ).length ) { + $body.find( '.classifai-panel' ).click(); + } + } ); cy.get( '#classifai-audio-controls__preview-btn' ).should( shouldExist ); } ); @@ -417,17 +412,17 @@ Cypress.Commands.add( const shouldExist = enabled ? 'exist' : 'not.exist'; // Verify with Image processing features in attachment metabox. cy.visit( options.imageEditLink ); - cy.get( '.misc-publishing-actions label[for=rescan-captions]' ).should( - shouldExist - ); - cy.get( '.misc-publishing-actions label[for=rescan-tags]' ).should( + cy.get( + '#classifai_image_processing label[for=rescan-captions]' + ).should( shouldExist ); + cy.get( '#classifai_image_processing label[for=rescan-tags]' ).should( shouldExist ); - cy.get( '.misc-publishing-actions label[for=rescan-ocr]' ).should( + cy.get( '#classifai_image_processing label[for=rescan-ocr]' ).should( shouldExist ); cy.get( - '.misc-publishing-actions label[for=rescan-smart-crop]' + '#classifai_image_processing label[for=rescan-smart-crop]' ).should( shouldExist ); // Verify with Image processing features in media model. @@ -463,3 +458,45 @@ Cypress.Commands.add( 'enableClassicEditor', () => { } } ); } ); + +Cypress.Commands.add( + 'createClassicPost', + ( { + postType = 'post', + title = 'Test Post', + content = 'Test content', + status = 'publish', + beforeSave, + } ) => { + cy.visit( `/wp-admin/post-new.php?post_type=${ postType }` ); + + cy.get( '#title' ).click().clear().type( title ); + + cy.get( '#content_ifr' ).then( ( $iframe ) => { + const doc = $iframe.contents().find( 'body#tinymce' ); + cy.wrap( doc ).find( 'p:last-child' ).type( content ); + } ); + + if ( 'undefined' !== typeof beforeSave ) { + beforeSave(); + } + + cy.intercept( 'POST', '/wp-admin/post.php', ( req ) => { + req.alias = 'savePost'; + } ); + + if ( 'draft' === status ) { + cy.get( '#save-post' ) + .should( 'not.have.class', 'disabled' ) + .click(); + } else { + cy.get( '#publish' ).should( 'not.have.class', 'disabled' ).click(); + } + + cy.wait( '@savePost' ).then( ( response ) => { + const body = new URLSearchParams( response.request?.body ); + const id = body.get( 'post_ID' ); + cy.wrap( id ); + } ); + } +);