self::is_configured(),
'status' => self::is_configured() ? (string) $state['status'] : 'not_configured',
'source' => self::constant_key() ? 'configuration' : ( ! empty( $state['api_key'] ) ? 'wordpress' : 'none' ),
'model' => self::get_builder_model(),
);
}
/** Return the selected Builder AI model after the existing developer filter. */
public static function get_builder_model() {
$state = self::state();
$model = isset( $state['model'] ) ? self::normalize_model( $state['model'] ) : self::DEFAULT_MODEL;
$model = preg_replace( '/[^A-Za-z0-9._:-]/', '', (string) apply_filters( 'divi_powers_openai_builder_model', $model ) );
return '' !== $model ? $model : self::DEFAULT_MODEL;
}
/** Return a short customer-facing label for the active Builder model. */
public static function get_builder_model_label() {
$model = self::get_builder_model();
$options = self::model_options();
return isset( $options[ $model ] ) ? $options[ $model ]['short_label'] : $model;
}
/**
* Return the key for server-side requests only.
*
* @return string|WP_Error
*/
public static function get_api_key() {
$constant = self::constant_key();
if ( $constant ) {
return $constant;
}
$state = self::state();
if ( empty( $state['api_key'] ) || ! is_string( $state['api_key'] ) ) {
return new WP_Error( 'divi_powers_openai_missing', __( 'Add an OpenAI API key before testing the connection.', '5d-powers-for-divi' ) );
}
return self::decrypt( $state['api_key'] );
}
/** Save or replace the encrypted administrator-provided key. */
public function handle_save() {
$this->require_admin();
if ( self::constant_key() ) {
$this->redirect( 'constant' );
}
$key = isset( $_POST['openai_api_key'] ) ? trim( (string) wp_unslash( $_POST['openai_api_key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- require_admin() verifies the nonce; preserving the secret byte-for-byte is required before strict format validation and encryption.
if ( strlen( $key ) < 20 || strlen( $key ) > 512 || preg_match( '/\s/', $key ) ) {
$this->redirect( 'invalid' );
}
$encrypted = self::encrypt( $key );
unset( $key );
if ( is_wp_error( $encrypted ) ) {
$this->redirect( 'crypto_error' );
}
$state = self::state();
$state['api_key'] = $encrypted;
$state['status'] = 'not_tested';
$state['last_checked'] = '';
$state['last_error'] = '';
update_option( self::OPTION, $state, false );
$this->redirect( 'saved' );
}
/** Save the administrator's preferred model for every Builder AI stage. */
public function handle_model_save() {
$this->require_admin();
$model = isset( $_POST['openai_model'] ) ? sanitize_text_field( wp_unslash( $_POST['openai_model'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- require_admin() verifies the nonce immediately above.
$options = self::model_options();
if ( ! isset( $options[ $model ] ) ) {
$this->redirect( 'invalid_model' );
}
$state = self::state();
$state['model'] = $model;
update_option( self::OPTION, $state, false );
$this->redirect( 'model_saved' );
}
/** Test the stored key from WordPress without returning it to the browser. */
public function handle_test() {
$this->require_admin();
$result = $this->test_connection();
$this->redirect( is_wp_error( $result ) ? 'test_failed' : 'connected' );
}
/** Remove the database-backed key and its connection status. */
public function handle_remove() {
$this->require_admin();
if ( self::constant_key() ) {
$this->redirect( 'constant' );
}
$state = self::state();
$state['api_key'] = '';
$state['status'] = 'not_configured';
$state['last_checked'] = '';
$state['last_error'] = '';
update_option( self::OPTION, $state, false );
$this->redirect( 'removed' );
}
/** Load the local provider settings style on the Builder AI screen. */
public function enqueue_guide_assets( $hook ) {
if ( in_array( $hook, array( 'divi-powers_page_divi-powers-connection', 'admin_page_divi-powers-connection' ), true ) && current_user_can( 'manage_options' ) ) {
wp_enqueue_style( 'divi-powers-connection', DIVI_POWERS_URL . 'assets/css/connection.css', array(), DIVI_POWERS_VERSION );
}
}
/** Render the optional on-site OpenAI connection. */
public function render_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$state = self::state();
$configured = self::is_configured();
$from_config = (bool) self::constant_key();
$model = self::normalize_model( $state['model'] );
$models = self::model_options();
$status = $configured ? (string) $state['status'] : 'not_configured';
if ( $from_config && 'not_configured' === $status ) {
$status = 'not_tested';
}
$labels = array(
'connected' => __( 'Connected', '5d-powers-for-divi' ),
'not_tested' => __( 'Saved — test the connection', '5d-powers-for-divi' ),
'error' => __( 'Connection needs attention', '5d-powers-for-divi' ),
'not_configured' => __( 'Not connected', '5d-powers-for-divi' ),
);
$message = isset( $_GET['dp_ai_message'] ) ? sanitize_key( wp_unslash( $_GET['dp_ai_message'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
?>
render_message( $message, $state ); ?>
↗
save_test_status( 'error', $key->get_error_message() );
return $key;
}
$response = wp_remote_get(
self::API_URL,
array(
'timeout' => 20,
'redirection' => 0,
'limit_response_size' => self::MAX_RESPONSE_BYTES,
'headers' => array(
'Authorization' => 'Bearer ' . $key,
'Accept' => 'application/json',
),
)
);
unset( $key );
if ( is_wp_error( $response ) ) {
$error = __( 'WordPress could not reach OpenAI. Check the site connection and try again.', '5d-powers-for-divi' );
$this->save_test_status( 'error', $error );
return new WP_Error( 'divi_powers_openai_network', $error );
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code >= 200 && $code < 300 ) {
$this->save_test_status( 'connected', '' );
return true;
}
$messages = array(
401 => __( 'OpenAI did not accept this API key. Replace it and try again.', '5d-powers-for-divi' ),
403 => __( 'This API key does not have permission to use the OpenAI API.', '5d-powers-for-divi' ),
429 => __( 'OpenAI received the request but the account is currently rate-limited or out of API credit.', '5d-powers-for-divi' ),
);
$error = isset( $messages[ $code ] ) ? $messages[ $code ] : __( 'OpenAI could not verify the connection. Try again in a moment.', '5d-powers-for-divi' );
$this->save_test_status( 'error', $error );
return new WP_Error( 'divi_powers_openai_http', $error, array( 'status' => $code ) );
}
/** Persist only a bounded, non-secret connection result. */
private function save_test_status( $status, $error ) {
$state = self::state();
$state['status'] = 'connected' === $status ? 'connected' : 'error';
$state['last_checked'] = gmdate( 'c' );
$state['last_error'] = substr( sanitize_text_field( (string) $error ), 0, 300 );
update_option( self::OPTION, $state, false );
}
/** Display one plain-language result on the settings card. */
private function render_message( $message, array $state ) {
$success = array(
'saved' => __( 'API key saved. Test the connection when you are ready.', '5d-powers-for-divi' ),
'connected' => __( 'Connected to OpenAI. The key is ready for Visual Builder AI.', '5d-powers-for-divi' ),
'removed' => __( 'The saved OpenAI API key was removed.', '5d-powers-for-divi' ),
'model_saved' => __( 'Builder AI model saved.', '5d-powers-for-divi' ),
);
$errors = array(
'invalid' => __( 'Enter a complete OpenAI API key without spaces.', '5d-powers-for-divi' ),
'invalid_model' => __( 'Choose one of the available Builder AI models.', '5d-powers-for-divi' ),
'crypto_error' => __( 'WordPress could not securely save the key on this server.', '5d-powers-for-divi' ),
'constant' => __( 'This key is controlled by the WordPress configuration and cannot be changed here.', '5d-powers-for-divi' ),
'test_failed' => ! empty( $state['last_error'] ) ? $state['last_error'] : __( 'The OpenAI connection test failed.', '5d-powers-for-divi' ),
);
if ( isset( $success[ $message ] ) ) {
echo '' . esc_html( $success[ $message ] ) . '
';
} elseif ( isset( $errors[ $message ] ) ) {
echo '' . esc_html( $errors[ $message ] ) . '
';
}
}
/** Require an administrator and a one-time WordPress form token. */
private function require_admin() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to configure the OpenAI connection.', '5d-powers-for-divi' ) );
}
check_admin_referer( self::NONCE_ACTION );
}
/** Return to the Divi Powers connection page with a bounded status code. */
private function redirect( $message ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'divi-powers-connection',
'dp_ai_message' => sanitize_key( $message ),
),
admin_url( 'admin.php' )
)
);
exit;
}
/** Read the non-autoloaded encrypted-key option with safe defaults. */
private static function state() {
$state = get_option( self::OPTION, array() );
$state = is_array( $state ) ? $state : array();
return wp_parse_args(
$state,
array(
'api_key' => '',
'status' => 'not_configured',
'last_checked' => '',
'last_error' => '',
'model' => self::DEFAULT_MODEL,
)
);
}
/**
* Return Builder-compatible models approved by Divi Powers and available
* to the connected OpenAI project.
*
* The built-in registry is the offline fallback. The canonical registry
* lets Divi Powers publish support for future models without requiring a
* plugin release.
*/
private static function model_options() {
$models = array(
'gpt-6-astra' => array(
'label' => __( 'GPT-6 Astra — Maximum quality, highest cost', '5d-powers-for-divi' ),
'short_label' => __( 'GPT-6 Astra', '5d-powers-for-divi' ),
),
'gpt-5.6-sol' => array(
'label' => __( 'GPT-5.6 Sol — Best everyday quality (default)', '5d-powers-for-divi' ),
'short_label' => __( 'GPT-5.6 Sol', '5d-powers-for-divi' ),
),
'gpt-5.6-terra' => array(
'label' => __( 'GPT-5.6 Terra — Balanced cost and quality', '5d-powers-for-divi' ),
'short_label' => __( 'GPT-5.6 Terra', '5d-powers-for-divi' ),
),
'gpt-5.6-luna' => array(
'label' => __( 'GPT-5.6 Luna — Lowest cost', '5d-powers-for-divi' ),
'short_label' => __( 'GPT-5.6 Luna', '5d-powers-for-divi' ),
),
);
$remote_models = self::remote_model_options();
if ( ! empty( $remote_models ) ) {
$models = array_replace( $models, $remote_models );
}
$available_ids = self::available_openai_model_ids();
if ( empty( $available_ids ) ) {
return $models;
}
$available = array();
foreach ( $models as $model_id => $details ) {
if ( in_array( $model_id, $available_ids, true ) ) {
$available[ $model_id ] = $details;
}
}
// Keep an existing selection visible so an administrator can replace it
// if OpenAI later removes access to that model.
$state = self::state();
$selected = isset( $state['model'] ) ? sanitize_text_field( (string) $state['model'] ) : '';
if ( $selected && isset( $models[ $selected ] ) && ! isset( $available[ $selected ] ) ) {
$models[ $selected ]['label'] .= __( ' — Unavailable for this API key', '5d-powers-for-divi' );
$available[ $selected ] = $models[ $selected ];
}
return ! empty( $available ) ? $available : $models;
}
/** Fetch and validate the canonical Divi Powers model registry. */
private static function remote_model_options() {
if ( ! Divi_Powers_Plugin::instance()->is_power_enabled( 'model_registry' ) ) {
return array();
}
$cache_key = 'divi_powers_openai_registry_v1';
$cached = get_site_transient( $cache_key );
if ( is_array( $cached ) ) {
return $cached;
}
$response = wp_safe_remote_get(
'https://divipowers.com/wp-content/plugins/divi-powers/assets/openai-models.json',
array(
'timeout' => 5,
'redirection' => 0,
'limit_response_size' => self::MAX_RESPONSE_BYTES,
'headers' => array( 'Accept' => 'application/json' ),
'user-agent' => 'Divi Powers/' . DIVI_POWERS_VERSION,
)
);
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
// Avoid repeating a failed lookup for every label rendered in the Builder.
set_site_transient( $cache_key, array(), 5 * MINUTE_IN_SECONDS );
return array();
}
$data = json_decode( (string) wp_remote_retrieve_body( $response ), true );
$items = is_array( $data ) && isset( $data['models'] ) && is_array( $data['models'] ) ? $data['models'] : array();
$models = array();
foreach ( array_slice( $items, 0, 20 ) as $item ) {
if ( ! is_array( $item ) || empty( $item['builder_compatible'] ) ) {
continue;
}
$model_id = isset( $item['id'] ) ? preg_replace( '/[^A-Za-z0-9._:-]/', '', (string) $item['id'] ) : '';
$label = isset( $item['label'] ) ? sanitize_text_field( (string) $item['label'] ) : '';
$short_label = isset( $item['short_label'] ) ? sanitize_text_field( (string) $item['short_label'] ) : '';
if ( '' === $model_id || '' === $label || '' === $short_label || strlen( $model_id ) > 100 || strlen( $label ) > 160 || strlen( $short_label ) > 80 ) {
continue;
}
$models[ $model_id ] = array(
'label' => $label,
'short_label' => $short_label,
);
}
set_site_transient( $cache_key, $models, empty( $models ) ? 5 * MINUTE_IN_SECONDS : 12 * HOUR_IN_SECONDS );
return $models;
}
/**
* Return model IDs exposed to this API key, cached per key for 12 hours.
*
* Failure intentionally returns an empty array so the approved offline
* registry remains usable during a temporary OpenAI outage.
*/
private static function available_openai_model_ids() {
if ( ! self::is_configured() ) {
return array();
}
$key = self::get_api_key();
if ( is_wp_error( $key ) ) {
return array();
}
$cache_key = 'divi_powers_openai_models_' . substr( hash( 'sha256', $key ), 0, 24 );
$cached = get_site_transient( $cache_key );
if ( is_array( $cached ) ) {
unset( $key );
return $cached;
}
$response = wp_remote_get(
self::API_URL,
array(
'timeout' => 5,
'redirection' => 0,
'limit_response_size' => self::MAX_RESPONSE_BYTES,
'headers' => array(
'Authorization' => 'Bearer ' . $key,
'Accept' => 'application/json',
),
)
);
unset( $key );
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
set_site_transient( $cache_key, array(), 5 * MINUTE_IN_SECONDS );
return array();
}
$data = json_decode( (string) wp_remote_retrieve_body( $response ), true );
$rows = is_array( $data ) && isset( $data['data'] ) && is_array( $data['data'] ) ? $data['data'] : array();
$ids = array();
foreach ( array_slice( $rows, 0, 1000 ) as $row ) {
$model_id = is_array( $row ) && isset( $row['id'] ) ? preg_replace( '/[^A-Za-z0-9._:-]/', '', (string) $row['id'] ) : '';
if ( '' !== $model_id && strlen( $model_id ) <= 100 ) {
$ids[] = $model_id;
}
}
$ids = array_values( array_unique( $ids ) );
set_site_transient( $cache_key, $ids, empty( $ids ) ? 5 * MINUTE_IN_SECONDS : 12 * HOUR_IN_SECONDS );
return $ids;
}
/** Normalize a stored selection without allowing arbitrary browser input. */
private static function normalize_model( $model ) {
$model = sanitize_text_field( (string) $model );
$options = self::model_options();
return isset( $options[ $model ] ) ? $model : self::DEFAULT_MODEL;
}
/** Support hosts that prefer keeping the key outside the WordPress database. */
private static function constant_key() {
if ( ! defined( 'DIVI_POWERS_OPENAI_API_KEY' ) || ! is_string( DIVI_POWERS_OPENAI_API_KEY ) ) {
return '';
}
return trim( DIVI_POWERS_OPENAI_API_KEY );
}
/** Encrypt a key with authenticated encryption derived from WordPress salts. */
private static function encrypt( $plaintext ) {
$key = self::encryption_key();
try {
if ( function_exists( 'sodium_crypto_secretbox' ) ) {
$nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
$cipher = sodium_crypto_secretbox( $plaintext, $nonce, $key );
return 's1:' . base64_encode( $nonce . $cipher );
}
if ( function_exists( 'openssl_encrypt' ) ) {
$iv = random_bytes( 12 );
$tag = '';
$cipher = openssl_encrypt( $plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag );
if ( false !== $cipher && 16 === strlen( $tag ) ) {
return 'o1:' . base64_encode( $iv . $tag . $cipher );
}
}
} catch ( Throwable $error ) {
return new WP_Error( 'divi_powers_openai_encrypt', __( 'The API key could not be encrypted.', '5d-powers-for-divi' ) );
}
return new WP_Error( 'divi_powers_openai_crypto_missing', __( 'This server does not provide a supported encryption library.', '5d-powers-for-divi' ) );
}
/** Decrypt and authenticate the database-backed key. */
private static function decrypt( $payload ) {
$key = self::encryption_key();
$raw = base64_decode( substr( $payload, 3 ), true );
$type = substr( $payload, 0, 3 );
if ( false === $raw ) {
return new WP_Error( 'divi_powers_openai_decrypt', __( 'The saved API key could not be read. Replace it and try again.', '5d-powers-for-divi' ) );
}
if ( 's1:' === $type && function_exists( 'sodium_crypto_secretbox_open' ) && strlen( $raw ) > SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ) {
$nonce = substr( $raw, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
$plain = sodium_crypto_secretbox_open( substr( $raw, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES ), $nonce, $key );
return false === $plain ? new WP_Error( 'divi_powers_openai_decrypt', __( 'The saved API key could not be verified. Replace it and try again.', '5d-powers-for-divi' ) ) : $plain;
}
if ( 'o1:' === $type && function_exists( 'openssl_decrypt' ) && strlen( $raw ) > 28 ) {
$iv = substr( $raw, 0, 12 );
$tag = substr( $raw, 12, 16 );
$plain = openssl_decrypt( substr( $raw, 28 ), 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag );
return false === $plain ? new WP_Error( 'divi_powers_openai_decrypt', __( 'The saved API key could not be verified. Replace it and try again.', '5d-powers-for-divi' ) ) : $plain;
}
return new WP_Error( 'divi_powers_openai_decrypt', __( 'The saved API key uses an unsupported encryption format.', '5d-powers-for-divi' ) );
}
/** Derive a stable 256-bit key without storing another secret. */
private static function encryption_key() {
return hash( 'sha256', wp_salt( 'auth' ) . '|' . wp_salt( 'secure_auth' ) . '|divi-powers-openai-v1', true );
}
}