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 @@
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' : '';
- ?>
-
-
-
+
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;
+ ?>
+
+
+
+
+
+
+
+
+
+
+ ' . 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;
}
-
?>
+
- >
+
>
@@ -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 &&
) }
) }
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'
)
) {
$( '