<?php
namespace Classifai\Features;
use Classifai\Services\LanguageProcessing;
use Classifai\Providers\Watson\NLU;
use Classifai\Providers\OpenAI\Embeddings as OpenAIEmbeddings;
use Classifai\Providers\Azure\Embeddings as AzureEmbeddings;
use WP_REST_Server;
use WP_REST_Request;
use WP_Error;
use function Classifai\get_post_statuses_for_language_settings;
use function Classifai\get_post_types_for_language_settings;
use function Classifai\check_term_permissions;
use function Classifai\get_classification_feature_enabled;
use function Classifai\get_classification_feature_taxonomy;
use function Classifai\get_asset_info;
use function Classifai\get_classification_mode;
/**
* Class Classification
*/
class Classification extends Feature {
/**
* ID of the current feature.
*
* @var string
*/
const ID = 'feature_classification';
/**
* Constructor.
*/
public function __construct() {
$this->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' ),
OpenAIEmbeddings::ID => __( 'OpenAI Embeddings', 'classifai' ),
AzureEmbeddings::ID => __( 'Azure OpenAI Embeddings', '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() {
$post_types = $this->get_supported_post_types();
if ( ! empty( $post_types ) ) {
foreach ( $post_types as $post_type ) {
add_action( 'rest_after_insert_' . $post_type, [ $this, 'rest_after_insert' ] );
}
}
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
add_action( 'classifai_after_feature_settings_form', [ $this, 'render_previewer' ] );
add_action( 'rest_api_init', [ $this, 'add_process_content_meta_to_rest_api' ] );
add_action( 'wp_ajax_classifai_get_post_search_results', array( $this, 'get_post_search_results' ) );
add_filter( 'default_post_metadata', [ $this, 'default_post_metadata' ], 10, 3 );
// Support the Classic Editor.
add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ], 10, 2 );
add_action( 'save_post', [ $this, 'save_meta_box' ] );
add_action( 'admin_post_classifai_classify_post', array( $this, 'classifai_classify_post' ) );
add_action( 'admin_notices', [ $this, 'show_error_if' ] );
add_filter( 'removable_query_args', [ $this, 'removable_query_args' ] );
}
/**
* Register any needed endpoints.
*/
public function register_endpoints() {
$post_types = $this->get_supported_post_types();
foreach ( $post_types as $post_type ) {
register_meta(
$post_type,
'_classifai_error',
[
'show_in_rest' => true,
'single' => true,
'auth_callback' => '__return_true',
]
);
}
register_rest_route(
'classifai/v1',
'classify/(?P<id>\d+)',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'rest_endpoint_callback' ],
'args' => array(
'id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID to classify.', 'classifai' ),
),
'linkTerms' => array(
'type' => 'boolean',
'description' => esc_html__( 'Whether to link terms or not.', 'classifai' ),
'default' => true,
),
),
'permission_callback' => [ $this, 'classify_permissions_check' ],
]
);
}
/**
* Check if a given request has access to run classification.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function classify_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;
}
// For all enabled features, ensure the user has proper permissions to add/edit terms.
$provider_instance = $this->get_feature_provider_instance();
if ( empty( $provider_instance->nlu_features ) ) {
return new WP_Error( 'not_enabled', esc_html__( 'Classification not configured correctly for the selected provider.', 'classifai' ) );
}
foreach ( $provider_instance->nlu_features as $feature_name => $feature ) {
if ( ! get_classification_feature_enabled( $feature_name ) ) {
continue;
}
$taxonomy = get_classification_feature_taxonomy( $feature_name );
$permission = check_term_permissions( $taxonomy );
if ( is_wp_error( $permission ) ) {
return $permission;
}
}
$post_status = get_post_status( $post_id );
$supported = $this->get_supported_post_types();
$post_statuses = $this->get_supported_post_statuses();
// Check if processing allowed.
if (
! in_array( $post_status, $post_statuses, true ) ||
! in_array( $post_type, $supported, true ) ||
! $this->is_feature_enabled()
) {
return new WP_Error( 'not_enabled', esc_html__( 'Classification not enabled for current item.', '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/classify' ) === 0 ) {
$results = $this->run(
$request->get_param( 'id' ),
'classify',
[
'link_terms' => $request->get_param( 'linkTerms' ),
]
);
// Save results or return the results that need saved.
if ( ! is_wp_error( $results ) ) {
$results = $this->save( $request->get_param( 'id' ), $results, $request->get_param( 'linkTerms' ) ?? true );
}
return rest_ensure_response(
[
'terms' => $results,
'feature_taxonomies' => $this->get_all_feature_taxonomies(),
]
);
}
return parent::rest_endpoint_callback( $request );
}
/**
* Save or return the classification results.
*
* If $link is true, we link the terms to the item. If
* it is false, we just return the terms that need linked
* so they can show in the UI.
*
* @param int $post_id The post ID.
* @param array $results Term results
* @param bool $link Whether to link the terms or not.
* @return array|WP_Error
*/
public function save( int $post_id, array $results, bool $link = true ) {
$provider_instance = $this->get_feature_provider_instance();
/**
* Filter results to be saved.
*
* @since 3.1.0
* @hook classifai_feature_classification_pre_save_results
*
* @param {array} $supported Term results.
* @param {int} $post_id Post ID.
* @param {bool} $link Whether to link the terms or not.
* @param {object} $this Current instance of the class.
*
* @return {array} Term results.
*/
$results = apply_filters( 'classifai_' . static::ID . '_pre_save_results', $results, $post_id, $link, $this );
switch ( $provider_instance::ID ) {
case NLU::ID:
$results = $provider_instance->link( $post_id, $results, $link );
break;
case AzureEmbeddings::ID:
case OpenAIEmbeddings::ID:
$results = $provider_instance->set_terms( $post_id, $results, $link );
break;
}
return $results;
}
/**
* Run classification after an item has been inserted via REST.
*
* @param \WP_Post $post Post object.
*/
public function rest_after_insert( \WP_Post $post ) {
$supported_post_types = $this->get_supported_post_types();
$post_statuses = $this->get_supported_post_statuses();
// Ensure the post type and status is allowed.
if (
! in_array( $post->post_type, $supported_post_types, true ) ||
! in_array( $post->post_status, $post_statuses, true )
) {
return;
}
// Check if processing on save is disabled.
if ( 'no' === get_post_meta( $post->ID, '_classifai_process_content', true ) ) {
return;
}
$results = $this->run( $post->ID, 'classify' );
if ( ! empty( $results ) && ! is_wp_error( $results ) ) {
$this->save( $post->ID, $results );
delete_post_meta( $post->ID, '_classifai_error' );
} elseif ( is_wp_error( $results ) ) {
update_post_meta(
$post->ID,
'_classifai_error',
wp_json_encode(
[
'code' => $results->get_error_code(),
'message' => $results->get_error_message(),
]
)
);
}
}
/**
* Enqueue the admin scripts.
*/
public function enqueue_admin_assets() {
wp_enqueue_script(
'classifai-language-processing-script',
CLASSIFAI_PLUGIN_URL . 'dist/language-processing.js',
get_asset_info( 'language-processing', 'dependencies' ),
get_asset_info( 'language-processing', 'version' ),
true
);
wp_enqueue_style(
'classifai-language-processing-style',
CLASSIFAI_PLUGIN_URL . 'dist/language-processing.css',
array(),
get_asset_info( 'language-processing', 'version' ),
'all'
);
}
/**
* Enqueue editor assets.
*/
public function enqueue_editor_assets() {
global $post;
wp_enqueue_script(
'classifai-editor',
CLASSIFAI_PLUGIN_URL . 'dist/editor.js',
get_asset_info( 'editor', 'dependencies' ),
get_asset_info( 'editor', 'version' ),
true
);
if ( empty( $post ) ) {
return;
}
wp_enqueue_script(
'classifai-gutenberg-plugin',
CLASSIFAI_PLUGIN_URL . 'dist/gutenberg-plugin.js',
array_merge( get_asset_info( 'gutenberg-plugin', 'dependencies' ), array( 'lodash' ) ),
get_asset_info( 'gutenberg-plugin', 'version' ),
true
);
wp_add_inline_script(
'classifai-gutenberg-plugin',
sprintf(
'var classifaiPostData = %s;',
wp_json_encode(
[
'NLUEnabled' => $this->is_feature_enabled(),
'supportedPostTypes' => $this->get_supported_post_types(),
'supportedPostStatues' => $this->get_supported_post_statuses(),
'noPermissions' => ! is_user_logged_in() || ! current_user_can( 'edit_post', $post->ID ),
]
)
),
'before'
);
}
/**
* Add `classifai_process_content` to the REST API for view/edit.
*/
public function add_process_content_meta_to_rest_api() {
$supported_post_types = $this->get_supported_post_types();
register_rest_field(
$supported_post_types,
'classifai_process_content',
[
'get_callback' => function ( $data ) {
$process_content = get_post_meta( $data['id'], '_classifai_process_content', true );
return ( 'no' === $process_content ) ? 'no' : 'yes';
},
'update_callback' => function ( $value, $data ) {
$value = ( 'no' === $value ) ? 'no' : 'yes';
return update_post_meta( $data->ID, '_classifai_process_content', $value );
},
'schema' => [
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
]
);
}
/**
* Searches and returns posts.
*/
public function get_post_search_results() {
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : false;
if ( ! ( $nonce && wp_verify_nonce( $nonce, 'classifai-previewer-action' ) ) ) {
wp_send_json_error( esc_html__( 'Failed nonce check.', 'classifai' ) );
}
$search_term = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
$post_types = isset( $_POST['post_types'] ) ? explode( ',', sanitize_text_field( wp_unslash( $_POST['post_types'] ) ) ) : 'post';
$post_statuses = isset( $_POST['post_status'] ) ? explode( ',', sanitize_text_field( wp_unslash( $_POST['post_status'] ) ) ) : 'publish';
$posts = get_posts(
array(
'post_type' => $post_types,
'post_status' => $post_statuses,
's' => $search_term,
)
);
wp_send_json_success( $posts );
}
/**
* Add metabox to enable/disable language processing.
*
* @param string $post_type Post type.
* @param \WP_Post $post WP_Post object.
*/
public function add_meta_box( string $post_type, $post ) {
$supported_post_types = $this->get_supported_post_types();
$post_statuses = $this->get_supported_post_statuses();
$post_status = get_post_status( $post );
if (
in_array( $post_type, $supported_post_types, true ) &&
in_array( $post_status, $post_statuses, true )
) {
add_meta_box(
'classifai_language_processing_metabox',
__( 'ClassifAI Language Processing', 'classifai' ),
[ $this, 'render_meta_box' ],
null,
'side',
'high',
array( '__back_compat_meta_box' => true )
);
}
}
/**
* Render metabox content.
*
* @param \WP_Post $post WP_Post object.
*/
public function render_meta_box( \WP_Post $post ) {
wp_nonce_field( 'classifai_language_processing_meta_action', 'classifai_language_processing_meta' );
$process_content = get_post_meta( $post->ID, '_classifai_process_content', true );
$process_content = ( 'no' === $process_content ) ? 'no' : 'yes';
?>
<p>
<label for="classifai-process-content">
<input type="checkbox" value="yes" name="_classifai_process_content" id="classifai-process-content" <?php checked( $process_content, 'yes' ); ?> />
<?php esc_html_e( 'Automatically tag content on update', 'classifai' ); ?>
</label>
</p>
<div class="classifai-clasify-post-wrapper" style="display: none;">
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=classifai_classify_post&post_id=' . $post->ID ), 'classifai_classify_post_action', 'classifai_classify_post_nonce' ) ); ?>" class="button button-classify-post">
<?php esc_html_e( 'Suggest terms & tags', 'classifai' ); ?>
</a>
</div>
<?php
}
/**
* Handles saving the metabox.
*
* @param int $post_id Current post ID.
*/
public function save_meta_box( int $post_id ) {
if (
wp_is_post_autosave( $post_id ) ||
wp_is_post_revision( $post_id ) ||
! current_user_can( 'edit_post', $post_id )
) {
return;
}
if (
empty( $_POST['classifai_language_processing_meta'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['classifai_language_processing_meta'] ) ), 'classifai_language_processing_meta_action' )
) {
return;
}
$classifai_process_content = isset( $_POST['_classifai_process_content'] ) ? sanitize_key( wp_unslash( $_POST['_classifai_process_content'] ) ) : '';
if ( 'yes' !== $classifai_process_content ) {
update_post_meta( $post_id, '_classifai_process_content', 'no' );
} else {
update_post_meta( $post_id, '_classifai_process_content', 'yes' );
$results = $this->run( $post_id, 'classify' );
if ( ! empty( $results ) && ! is_wp_error( $results ) ) {
$this->save( $post_id, $results );
delete_post_meta( $post_id, '_classifai_error' );
} elseif ( is_wp_error( $results ) ) {
update_post_meta(
$post_id,
'_classifai_error',
wp_json_encode(
[
'code' => $results->get_error_code(),
'message' => $results->get_error_message(),
]
)
);
}
}
}
/**
* Classify post manually.
*
* Fires when the Classify button is clicked
* in the Classic Editor.
*/
public function classifai_classify_post() {
if (
empty( $_GET['classifai_classify_post_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['classifai_classify_post_nonce'] ) ), 'classifai_classify_post_action' )
) {
wp_die( esc_html__( 'You don\'t have permission to perform this operation.', 'classifai' ) );
}
$post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
if ( ! $post_id ) {
exit();
}
// Check to see if processing is disabled and overwrite that.
// Since we are manually classifying, we want to force this.
$enabled = get_post_meta( $post_id, '_classifai_process_content', true );
if ( 'yes' !== $enabled ) {
update_post_meta( $post_id, '_classifai_process_content', 'yes' );
}
$results = $this->run( $post_id, 'classify' );
// Ensure the processing value is changed back to what it was.
if ( 'yes' !== $enabled ) {
update_post_meta( $post_id, '_classifai_process_content', 'no' );
}
$classified = array();
if ( ! empty( $results ) && ! is_wp_error( $results ) ) {
$this->save( $post_id, $results );
$classified = array( 'classifai_classify' => 1 );
delete_post_meta( $post_id, '_classifai_error' );
} elseif ( is_wp_error( $results ) ) {
update_post_meta(
$post_id,
'_classifai_error',
wp_json_encode(
[
'code' => $results->get_error_code(),
'message' => $results->get_error_message(),
]
)
);
}
wp_safe_redirect( esc_url_raw( add_query_arg( $classified, get_edit_post_link( $post_id, 'edit' ) ) ) );
exit();
}
/**
* Outputs an admin notice with the error message if needed.
*/
public function show_error_if() {
global $post;
if ( empty( $post ) ) {
return;
}
$post_id = $post->ID;
if ( empty( $post_id ) ) {
return;
}
$error = get_post_meta( $post_id, '_classifai_error', true );
if ( ! empty( $error ) ) {
delete_post_meta( $post_id, '_classifai_error' );
$error = (array) json_decode( $error );
$code = ! empty( $error['code'] ) ? $error['code'] : 500;
$message = ! empty( $error['message'] ) ? $error['message'] : 'Unknown API error';
?>
<div class="notice notice-error is-dismissible">
<p>
<?php esc_html_e( 'Error: Failed to classify content.', 'classifai' ); ?>
</p>
<p>
<?php echo esc_html( $code ); ?>
-
<?php echo esc_html( $message ); ?>
</p>
</div>
<?php
}
// Display classify post success message for manually classified post.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$classified = isset( $_GET['classifai_classify'] ) ? intval( wp_unslash( $_GET['classifai_classify'] ) ) : 0;
if ( 1 === $classified ) {
$post_type = get_post_type_object( get_post_type( $post ) );
$post_type_label = esc_html__( 'Post', 'classifai' );
if ( $post_type ) {
$post_type_label = $post_type->labels->singular_name;
}
?>
<div class="notice notice-success is-dismissible">
<p>
<?php
// translators: %s is post type label.
printf( esc_html__( '%s classified successfully.', 'classifai' ), esc_html( $post_type_label ) );
?>
</p>
</div>
<?php
}
}
/**
* Sets the default value for the _classifai_process_content meta key.
*
* @param mixed $value The value get_metadata() should return - a single metadata value,
* or an array of values.
* @param int $object_id Object ID.
* @param string $meta_key Meta key.
* @return mixed
*/
public function default_post_metadata( $value, int $object_id, string $meta_key ) {
if ( '_classifai_process_content' === $meta_key ) {
if ( 'automatic_classification' === get_classification_mode() ) {
return 'yes';
} else {
return 'no';
}
}
return $value;
}
/**
* Add "classifai_classify" in list of query variable names to remove.
*
* @param array $removable_query_args An array of query variable names to remove from a URL.
* @return array
*/
public function removable_query_args( array $removable_query_args ): array {
$removable_query_args[] = 'classifai_classify';
return $removable_query_args;
}
/**
* Renders the previewer window for the feature.
*
* @param string $active_feature The active feature.
*/
public function render_previewer( string $active_feature ) {
if ( self::ID !== $active_feature || ! $this->is_feature_enabled() ) {
return;
}
$provider_instance = $this->get_feature_provider_instance();
$nlu_features = array();
$supported_post_statuses = $this->get_supported_post_statuses();
$supported_post_types = $this->get_supported_post_types();
$posts_to_preview = get_posts(
array(
'post_type' => $supported_post_types,
'post_status' => $supported_post_statuses,
'posts_per_page' => 10,
)
);
if ( ! empty( $provider_instance->nlu_features ) ) {
$nlu_features = $provider_instance->nlu_features;
}
?>
<div id="classifai-post-preview-app">
<h2><?php esc_html_e( 'Preview Language Processing', 'classifai' ); ?></h2>
<div id="classifai-post-preview-controls">
<select id="classifai-preview-post-selector">
<?php foreach ( $posts_to_preview as $post ) : ?>
<option value="<?php echo esc_attr( $post->ID ); ?>"><?php echo esc_html( $post->post_title ); ?></option>
<?php endforeach; ?>
</select>
<?php wp_nonce_field( 'classifai-previewer-action', 'classifai-previewer-nonce' ); ?>
<button type="button" class="button" id="get-classifier-preview-data-btn">
<span><?php esc_html_e( 'Preview', 'classifai' ); ?></span>
</button>
</div>
<div id="classifai-post-preview-wrapper">
<?php
foreach ( $nlu_features as $feature_slug => $feature ) :
if ( ! get_classification_feature_enabled( $feature_slug ) ) {
continue;
}
?>
<div class="tax-row tax-row--<?php echo esc_attr( $feature_slug ); ?>">
<div class="tax-type"><?php echo esc_html( $feature['feature'] ); ?></div>
</div>
<?php
endforeach;
?>
</div>
</div>
<?php
}
/**
* Get the description for the enable field.
*
* @return string
*/
public function get_enable_description(): string {
return esc_html__( 'Enables automatic content classification.', 'classifai' );
}
/**
* Add any needed custom fields.
*/
public function add_custom_settings_fields() {
$settings = $this->get_settings();
$provider_instance = $this->get_feature_provider_instance();
$nlu_features = array();
$post_statuses = get_post_statuses_for_language_settings();
$post_types = get_post_types_for_language_settings();
$post_type_options = array();
if ( ! empty( $provider_instance->nlu_features ) ) {
$nlu_features = $provider_instance->nlu_features;
}
foreach ( $post_types as $post_type ) {
$post_type_options[ $post_type->name ] = $post_type->label;
}
add_settings_field(
'classification_mode',
esc_html__( 'Classification mode', 'classifai' ),
[ $this, 'render_radio_group' ],
$this->get_option_name(),
$this->get_option_name() . '_section',
[
'label_for' => 'classification_mode',
'default_value' => $settings['classification_mode'],
'options' => array(
'manual_review' => __( 'Manual review', 'classifai' ),
'automatic_classification' => __( 'Automatic classification', 'classifai' ),
),
]
);
$method_options = array(
'recommended_terms' => __( 'Recommend terms even if they do not exist on the site', 'classifai' ),
'existing_terms' => __( 'Only recommend terms that already exist on the site', 'classifai' ),
);
// Embeddings only supports existing terms.
if ( isset( $settings['provider'] ) && ( OpenAIEmbeddings::ID === $settings['provider'] || AzureEmbeddings::ID === $settings['provider'] ) ) {
unset( $method_options['recommended_terms'] );
$settings['classification_method'] = 'existing_terms';
}
add_settings_field(
'classification_method',
esc_html__( 'Classification method', 'classifai' ),
[ $this, 'render_radio_group' ],
$this->get_option_name(),
$this->get_option_name() . '_section',
[
'label_for' => 'classification_method',
'default_value' => $settings['classification_method'],
'options' => $method_options,
]
);
foreach ( $nlu_features as $classify_by => $labels ) {
add_settings_field(
$classify_by,
esc_html( $labels['feature'] ),
[ $this, 'render_nlu_feature_settings' ],
$this->get_option_name(),
$this->get_option_name() . '_section',
[
'feature' => $classify_by,
'labels' => $labels,
'default_value' => $settings[ $classify_by ],
'post_types' => $settings['post_types'],
]
);
}
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' ),
]
);
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,
],
'classification_mode' => 'manual_review',
'classification_method' => 'recommended_terms',
'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();
$provider_instance = $this->get_feature_provider_instance();
$new_settings['classification_mode'] = sanitize_text_field( $new_settings['classification_mode'] ?? $settings['classification_mode'] );
$new_settings['classification_method'] = sanitize_text_field( $new_settings['classification_method'] ?? $settings['classification_method'] );
// Embeddings only supports existing terms.
if ( isset( $new_settings['provider'] ) && ( OpenAIEmbeddings::ID === $new_settings['provider'] || AzureEmbeddings::ID === $new_settings['provider'] ) ) {
$new_settings['classification_method'] = 'existing_terms';
}
$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'];
if ( ! empty( $provider_instance->nlu_features ) ) {
foreach ( array_keys( $provider_instance->nlu_features ) as $feature_name ) {
$new_settings[ $feature_name ] = absint( $new_settings[ $feature_name ] ?? $settings[ $feature_name ] );
$new_settings[ "{$feature_name}_threshold" ] = absint( $new_settings[ "{$feature_name}_threshold" ] ?? $settings[ "{$feature_name}_threshold" ] );
$new_settings[ "{$feature_name}_taxonomy" ] = sanitize_text_field( $new_settings[ "{$feature_name}_taxonomy" ] ?? $settings[ "{$feature_name}_taxonomy" ] );
}
}
return $new_settings;
}
/**
* Get all feature taxonomies.
*
* @return array
*/
public function get_all_feature_taxonomies(): array {
$feature_taxonomies = [];
$provider_instance = $this->get_feature_provider_instance();
if ( empty( $provider_instance->nlu_features ) ) {
return $feature_taxonomies;
}
foreach ( array_keys( $provider_instance->nlu_features ) as $feature_name ) {
if ( ! get_classification_feature_enabled( $feature_name ) ) {
continue;
}
$taxonomy = get_classification_feature_taxonomy( $feature_name );
$permission = check_term_permissions( $taxonomy );
if ( is_wp_error( $permission ) ) {
continue;
}
if ( 'post_tag' === $taxonomy ) {
$taxonomy = 'tags';
}
if ( 'category' === $taxonomy ) {
$taxonomy = 'categories';
}
$feature_taxonomies[] = $taxonomy;
}
return $feature_taxonomies;
}
/**
* Render the NLU features settings.
*
* @param array $args Settings for the inputs
*/
public function render_nlu_feature_settings( array $args ) {
$feature = $args['feature'];
$labels = $args['labels'];
$taxonomies = $this->get_supported_taxonomies( $args['post_types'] );
$features = $this->get_settings();
$taxonomy = isset( $features[ "{$feature}_taxonomy" ] ) ? $features[ "{$feature}_taxonomy" ] : $labels['taxonomy_default'];
// Enable classification type
$feature_args = [
'label_for' => $feature,
'input_type' => 'checkbox',
];
$threshold_args = [
'label_for' => "{$feature}_threshold",
'input_type' => 'number',
'default_value' => $labels['threshold_default'],
];
?>
<legend class="screen-reader-text">
<?php esc_html_e( 'Classification Taxonomy Settings', 'classifai' ); ?>
</legend>
<p>
<?php $this->render_input( $feature_args ); ?>
<label for="<?php echo esc_attr( $feature ); ?>">
<?php esc_html_e( 'Enable', 'classifai' ); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr( "{$feature}_threshold" ); ?>">
<?php echo esc_html( $labels['threshold'] ); ?>
</label><br/>
<?php $this->render_input( $threshold_args ); ?>
</p>
<?php if ( NLU::ID === $features['provider'] ) : ?>
<p>
<label for="classifai-settings-<?php echo esc_attr( "{$feature}_taxonomy" ); ?>">
<?php echo esc_html( $labels['taxonomy'] ); ?>
</label><br/>
<select id="classifai-settings-<?php echo esc_attr( "{$feature}_taxonomy" ); ?>" name="<?php echo esc_attr( $this->get_option_name() ); ?>[<?php echo esc_attr( "{$feature}_taxonomy" ); ?>]">
<?php foreach ( $taxonomies as $name => $singular_name ) : ?>
<option value="<?php echo esc_attr( $name ); ?>" <?php selected( $taxonomy, esc_attr( $name ) ); ?>>
<?php echo esc_html( $singular_name ); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<?php endif; ?>
<?php
}
/**
* Return the list of supported taxonomies
*
* @param array $post_types Array of supported post types.
* @return array
*/
public function get_supported_taxonomies( array $post_types = [] ): array {
$supported_post_types = [];
if ( ! empty( $post_types ) ) {
foreach ( $post_types as $post_type => $enabled ) {
if ( ! empty( $enabled ) ) {
$supported_post_types[] = $post_type;
}
}
}
$taxonomies = get_taxonomies( [], 'objects' );
$taxonomies = array_filter( $taxonomies, 'is_taxonomy_viewable' );
$supported = [];
foreach ( $taxonomies as $taxonomy ) {
// Remove this taxonomy if it doesn't support at least one of our post types.
if (
(
! empty( $supported_post_types ) &&
empty( array_intersect( $supported_post_types, $taxonomy->object_type ) )
) ||
'post_format' === $taxonomy->name
) {
continue;
}
$supported[ $taxonomy->name ] = $taxonomy->labels->singular_name;
}
/**
* Filter taxonomies shown in settings.
*
* @since 3.0.0
* @hook classifai_feature_classification_setting_taxonomies
*
* @param {array} $supported Array of supported taxonomies.
* @param {object} $this Current instance of the class.
*
* @return {array} Array of taxonomies.
*/
return apply_filters( 'classifai_' . static::ID . '_setting_taxonomies', $supported, $this );
}
/**
* Generates feature setting data required for migration from
* ClassifAI < 3.0.0 to 3.0.0
*
* @return array
*/
public function migrate_settings() {
$old_settings = get_option( 'classifai_watson_nlu', array() );
$new_settings = $this->get_default_settings();
if ( isset( $old_settings['authenticated'] ) && $old_settings['authenticated'] ) {
$new_settings['provider'] = 'ibm_watson_nlu';
// Status
if ( isset( $old_settings['enable_content_classification'] ) ) {
$new_settings['status'] = $old_settings['enable_content_classification'];
}
// Post types
if ( isset( $old_settings['post_types'] ) ) {
if ( is_array( $old_settings['post_types'] ) ) {
foreach ( $old_settings['post_types'] as $post_type => $value ) {
if ( 1 === $value ) {
$new_settings['post_types'][ $post_type ] = $post_type;
continue;
} elseif ( is_null( $value ) ) {
$new_settings['post_types'][ $post_type ] = '0';
continue;
}
$new_settings['post_types'][ $post_type ] = $value;
}
}
unset( $new_settings['post_types']['attachment'] );
}
// Post statuses
if ( isset( $old_settings['post_statuses'] ) ) {
if ( is_array( $old_settings['post_statuses'] ) ) {
foreach ( $old_settings['post_statuses'] as $post_status => $value ) {
if ( 1 === $value ) {
$new_settings['post_statuses'][ $post_status ] = $post_status;
continue;
} elseif ( is_null( $value ) ) {
$new_settings['post_statuses'][ $post_status ] = '0';
continue;
}
$new_settings['post_statuses'][ $post_status ] = $value;
}
}
}
// Roles
if ( isset( $old_settings['content_classification_roles'] ) ) {
$new_settings['roles'] = $old_settings['content_classification_roles'];
}
// Users
if ( isset( $old_settings['content_classification_users'] ) ) {
$new_settings['users'] = $old_settings['content_classification_users'];
}
// Provider.
if ( isset( $old_settings['credentials'] ) && isset( $old_settings['credentials']['watson_url'] ) ) {
$new_settings['ibm_watson_nlu']['endpoint_url'] = $old_settings['credentials']['watson_url'];
}
if ( isset( $old_settings['credentials'] ) && isset( $old_settings['credentials']['watson_username'] ) ) {
$new_settings['ibm_watson_nlu']['username'] = $old_settings['credentials']['watson_username'];
}
if ( isset( $old_settings['credentials'] ) && isset( $old_settings['credentials']['watson_password'] ) ) {
$new_settings['ibm_watson_nlu']['password'] = $old_settings['credentials']['watson_password'];
}
if ( isset( $old_settings['classification_mode'] ) ) {
$new_settings['classification_mode'] = $old_settings['classification_mode'];
}
if ( isset( $old_settings['classification_method'] ) ) {
$new_settings['classification_method'] = $old_settings['classification_method'];
}
if ( isset( $old_settings['features'] ) ) {
foreach ( $old_settings['features'] as $feature => $value ) {
$new_settings[ $feature ] = $value;
}
}
if ( isset( $old_settings['authenticated'] ) ) {
$new_settings['ibm_watson_nlu']['authenticated'] = $old_settings['authenticated'];
}
if ( isset( $old_settings['content_classification_user_based_opt_out'] ) ) {
$new_settings['user_based_opt_out'] = $old_settings['content_classification_user_based_opt_out'];
}
} else {
$old_settings = get_option( 'classifai_openai_embeddings', array() );
if ( isset( $old_settings['enable_classification'] ) ) {
$new_settings['status'] = $old_settings['enable_classification'];
}
$new_settings['provider'] = 'openai_embeddings';
if ( isset( $old_settings['api_key'] ) ) {
$new_settings['openai_embeddings']['api_key'] = $old_settings['api_key'];
}
if ( isset( $old_settings['taxonomies'] ) ) {
foreach ( $old_settings['taxonomies'] as $feature => $value ) {
$new_settings[ $feature ] = $value;
}
}
if ( isset( $old_settings['authenticated'] ) ) {
$new_settings['openai_embeddings']['authenticated'] = $old_settings['authenticated'];
}
if ( isset( $old_settings['post_statuses'] ) ) {
$new_settings['post_statuses'] = $old_settings['post_statuses'];
}
if ( isset( $old_settings['post_types'] ) ) {
$new_settings['post_types'] = $old_settings['post_types'];
}
if ( isset( $old_settings['classification_roles'] ) ) {
$new_settings['roles'] = $old_settings['classification_roles'];
}
if ( isset( $old_settings['classification_users'] ) ) {
$new_settings['users'] = $old_settings['classification_users'];
}
if ( isset( $old_settings['classification_user_based_opt_out'] ) ) {
$new_settings['user_based_opt_out'] = $old_settings['classification_user_based_opt_out'];
}
}
return $new_settings;
}
}