Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1479dc5
feat: adds tools for importing PHP AI Client
JasonTheAdams Feb 5, 2026
2c842f1
feat: adds php-ai-client to includes
JasonTheAdams Feb 5, 2026
99efae9
feat: adds ai client
JasonTheAdams Feb 6, 2026
1c07c3e
refactor: moves prompt builder and renames directory
JasonTheAdams Feb 6, 2026
23f1af0
fix: handles support methods in an error state
JasonTheAdams Feb 6, 2026
42197b5
refactor: namespaces PSR classes and corrects versions
JasonTheAdams Feb 6, 2026
8a9d2c6
feat: adds wp_ai_client_prompt function
JasonTheAdams Feb 7, 2026
56c6873
refactor: corrects formatting issues
JasonTheAdams Feb 7, 2026
a5bd792
refactor: adds and runs third-party tree-shaking
JasonTheAdams Feb 11, 2026
242f9f9
test: corrects PHP 8.5 compatibility
JasonTheAdams Feb 11, 2026
7caa159
test: corrects formatting issues
JasonTheAdams Feb 11, 2026
0edbfef
test: adjusts reflection accessibility for PHP compatibility
JasonTheAdams Feb 11, 2026
ebbdc54
refactor: removes unnecessary polyfills
JasonTheAdams Feb 11, 2026
278f753
fix: adds missing translation functions and comments
JasonTheAdams Feb 11, 2026
85b1916
test: adds ai client util tests
JasonTheAdams Feb 11, 2026
c494c59
feat: adds AI settings screen
JasonTheAdams Feb 11, 2026
0e78c62
Adjust path filtering for code coverage reports.
desrosj Feb 12, 2026
1e2d52c
Merge branch 'trunk' into add/wp-ai-client
felixarntz Feb 14, 2026
9626b32
chore: explicitly lays out $prompt types
JasonTheAdams Feb 16, 2026
62b33aa
feat: locks in only supported stream from file mode
JasonTheAdams Feb 19, 2026
52d4963
refactor: simplifies to using array_find
JasonTheAdams Feb 19, 2026
472f69f
test: adds Prompt_Builder snake case test
JasonTheAdams Feb 20, 2026
b4f9bd7
test: fixes using_abilities tests
JasonTheAdams Feb 20, 2026
9c3b25e
fix: correctly identifies falsey cached values
JasonTheAdams Feb 20, 2026
d708bd2
refactor: moves function resolver and renames ai client folder
JasonTheAdams Feb 20, 2026
97f8598
refactor: switches to wp_safe_remote_request
JasonTheAdams Feb 20, 2026
9e0ebcc
refactor: uses str_starts_with to simplify
JasonTheAdams Feb 20, 2026
76bc7ba
Merge branch 'trunk' into add/wp-ai-client
felixarntz Feb 20, 2026
9364aca
Move AI client initialization out of require file block.
felixarntz Feb 20, 2026
87c0400
Add correct ticket annotations for tests.
felixarntz Feb 20, 2026
d97a86a
Translate messages from failed ability lookup or execution.
felixarntz Feb 20, 2026
582d0d0
Remove unnecessary X-Stream header exclusion.
felixarntz Feb 20, 2026
9098a6e
Yoda.
felixarntz Feb 20, 2026
5f3c5be
Mark infra classes as private.
felixarntz Feb 20, 2026
00ef1b8
Clean up abilities during testing.
felixarntz Feb 20, 2026
0e8a5dc
Make function_name_to_ability_name public.
felixarntz Feb 20, 2026
e8a2a2f
Add warning if invalid ability slug is passed to using_abilities.
felixarntz Feb 20, 2026
0774f6f
feat: always clones php ai client
JasonTheAdams Feb 20, 2026
c58351d
feat: updates PHP AI Client to 1.1.0
JasonTheAdams Feb 20, 2026
7f94810
refactor: uses new PHP AI Client http discovery system
JasonTheAdams Feb 20, 2026
76af7a8
test: fixes failing test
JasonTheAdams Feb 20, 2026
b5baa00
refactor: moves all ai-client classes into ai-client directory
JasonTheAdams Feb 20, 2026
6fa12cb
chore: adds php ai client tool readme
JasonTheAdams Feb 20, 2026
a2852ac
Merge branch 'add/wp-ai-client' into add/ai-settings-screen
JasonTheAdams Feb 20, 2026
18eba37
fix: registers settings in correct place
JasonTheAdams Feb 20, 2026
27cf7cf
Merge branch 'trunk' into add/ai-settings-screen
JasonTheAdams Feb 20, 2026
1bf9bc6
chore: corrects coverage file
JasonTheAdams Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/wp-admin/menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ function _add_plugin_file_editor_to_tools() {
$submenu['options-general.php'][30] = array( __( 'Media' ), 'manage_options', 'options-media.php' );
$submenu['options-general.php'][40] = array( __( 'Permalinks' ), 'manage_options', 'options-permalink.php' );
$submenu['options-general.php'][45] = array( __( 'Privacy' ), 'manage_privacy_options', 'options-privacy.php' );
if ( ! empty( $GLOBALS['wp_ai_client_credentials_manager']->get_all_cloud_providers_metadata() ) ) {
$submenu['options-general.php'][47] = array( __( 'AI Services' ), 'manage_options', 'options-ai.php' );
}

$_wp_last_utility_menu = 80; // The index of the last top-level menu in the utility menu group.

Expand Down
102 changes: 102 additions & 0 deletions src/wp-admin/options-ai.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php
/**
* AI Services settings administration panel.
*
* @package WordPress
* @subpackage Administration
* @since 7.0.0
*/

/** WordPress Administration Bootstrap */
require_once __DIR__ . '/admin.php';

if ( ! current_user_can( 'manage_options' ) ) {
wp_die( __( 'Sorry, you are not allowed to manage options for this site.' ) );
}

// Used in the HTML title tag.
$title = __( 'AI Services Settings' );
$parent_file = 'options-general.php';

$credentials_manager = $GLOBALS['wp_ai_client_credentials_manager'];

$cloud_providers = $credentials_manager->get_all_cloud_providers_metadata();

$settings_section = 'wp-ai-client-provider-credentials';

add_settings_section(
$settings_section,
'',
static function () {
?>
<p class="description">
<?php _e( 'Paste your API credentials for one or more AI providers you would like to use throughout your site.' ); ?>
</p>
<?php
},
'ai'
);

foreach ( $cloud_providers as $provider_metadata ) {
$provider_id = $provider_metadata->getId();
$provider_name = $provider_metadata->getName();
$provider_credentials_url = $provider_metadata->getCredentialsUrl();

$field_id = 'wp-ai-client-provider-api-key-' . $provider_id;
$field_args = array(
'type' => 'password',
'label_for' => $field_id,
'id' => $field_id,
'name' => WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS . '[' . $provider_id . ']',
);
if ( $provider_credentials_url ) {
$field_args['description'] = sprintf(
/* translators: 1: AI provider name, 2: URL to the provider's API credentials page. */
__( 'Create and manage your %1$s API keys in the <a href="%2$s" target="_blank" rel="noopener noreferrer">%1$s account settings<span class="screen-reader-text"> (opens in a new tab)</span></a>.' ),
$provider_name,
esc_url( $provider_credentials_url )
);
}

add_settings_field(
$field_id,
$provider_name,
'wp_ai_client_render_credential_field',
'ai',
$settings_section,
$field_args
);
}

$ai_help = '<p>' . __( 'This screen allows you to configure API credentials for AI service providers. These credentials are used by AI-powered features throughout your site.' ) . '</p>';
$ai_help .= '<p>' . __( 'You must click the Save Changes button at the bottom of the screen for new settings to take effect.' ) . '</p>';

get_current_screen()->add_help_tab(
array(
'id' => 'overview',
'title' => __( 'Overview' ),
'content' => $ai_help,
)
);

get_current_screen()->set_help_sidebar(
'<p><strong>' . __( 'For more information:' ) . '</strong></p>' .
'<p>' . __( '<a href="https://wordpress.org/support/forums/">Support forums</a>' ) . '</p>'
);

require_once ABSPATH . 'wp-admin/admin-header.php';

?>

<div class="wrap">
<h1><?php echo esc_html( $title ); ?></h1>

<form action="options.php" method="post">
<?php settings_fields( 'ai' ); ?>
<?php do_settings_sections( 'ai' ); ?>
<?php submit_button(); ?>
</form>

</div>

<?php require_once ABSPATH . 'wp-admin/admin-footer.php'; ?>
1 change: 1 addition & 0 deletions src/wp-admin/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
$allowed_options['misc'] = array();
$allowed_options['options'] = array();
$allowed_options['privacy'] = array();
$allowed_options['ai'] = array();

/**
* Filters whether the post-by-email functionality is enabled.
Expand Down
69 changes: 69 additions & 0 deletions src/wp-includes/ai-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,72 @@
function wp_ai_client_prompt( $prompt = null ) {
return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt );
}

/**
* Renders a credential input field for the AI Services settings page.
*
* @since 7.0.0
* @access private
*
* @param array $args {
* Field arguments set up during add_settings_field().
*
* @type string $type Input type. Default 'text'.
* @type string $id Field ID attribute.
* @type string $name Field name attribute, may include array notation.
* @type string $description Optional. Field description HTML.
* }
*/
function wp_ai_client_render_credential_field( $args ) {
$type = isset( $args['type'] ) ? $args['type'] : 'text';
$id = isset( $args['id'] ) ? $args['id'] : '';
$name = isset( $args['name'] ) ? $args['name'] : '';
$description = isset( $args['description'] ) ? $args['description'] : '';
$description_id = $id . '_description';

if ( str_contains( $name, '[' ) ) {
$parts = explode( '[', $name, 2 );
$option = get_option( $parts[0] );
$subkey = trim( $parts[1], ']' );
if ( is_array( $option ) && isset( $option[ $subkey ] ) && is_string( $option[ $subkey ] ) ) {
$value = $option[ $subkey ];
} else {
$value = '';
}
} else {
$option = get_option( $name );
$value = is_string( $option ) ? $option : '';
}

?>
<input
type="<?php echo esc_attr( $type ); ?>"
id="<?php echo esc_attr( $id ); ?>"
name="<?php echo esc_attr( $name ); ?>"
Comment on lines +75 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a chance both $id and $name are empty. Might be better to not render these attributes in that case instead of rendering them as empty attributes

value="<?php echo esc_attr( $value ); ?>"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something we just fixed in ClassifAI that came in from a security review was to ensure credentials (like API keys) were obfuscated prior to rendering in our settings screens. While I know this will render as a password input, you can still easily inspect the source and get the value of the input.

I also realize only those with the manage_options cap can access this page which limits scope but there still could be a scenario where someone that we don't want to have those API keys has the right access to get to this page and then could copy keys after inspecting the source.

I do think this is an edge case so not saying it needs to be changed, just fresh on my mind as I just adjusted this behavior myself a few days ago :)

class="regular-text"
<?php echo $description ? 'aria-describedby="' . esc_attr( $description_id ) . '"' : ''; ?>
>
<?php

if ( $description ) {
$allowed_html = array(
'a' => array(
'class' => array(),
'href' => array(),
'target' => array(),
'rel' => array(),
),
'strong' => array(),
'em' => array(),
'span' => array(
'class' => array(),
),
);
?>
<p id="<?php echo esc_attr( $description_id ); ?>" class="description">
<?php echo wp_kses( $description, $allowed_html ); ?>
</p>
<?php
}
}
Loading
Loading