release: v1.3.0

This commit is contained in:
noschmarrn 2026-04-17 17:57:39 +00:00
parent 5139e5ad29
commit a6043fe28a
7 changed files with 91 additions and 29 deletions

View file

@ -304,6 +304,7 @@ define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' ); define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
define( 'BREZNGEO_GEMINI_KEY', 'AI...' ); define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
define( 'BREZNGEO_GROK_KEY', 'xai-...' ); define( 'BREZNGEO_GROK_KEY', 'xai-...' );
define( 'BREZNGEO_OPENROUTER_KEY', 'sk-or-...' );
``` ```
### CSRF-Schutz und Capability Checks ### CSRF-Schutz und Capability Checks

View file

@ -413,6 +413,7 @@ define( 'BREZNGEO_OPENAI_KEY', 'sk-...' );
define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' ); define( 'BREZNGEO_ANTHROPIC_KEY', 'sk-ant-...' );
define( 'BREZNGEO_GEMINI_KEY', 'AI...' ); define( 'BREZNGEO_GEMINI_KEY', 'AI...' );
define( 'BREZNGEO_GROK_KEY', 'xai-...' ); define( 'BREZNGEO_GROK_KEY', 'xai-...' );
define( 'BREZNGEO_OPENROUTER_KEY', 'sk-or-...' );
``` ```
In the admin UI, keys are always displayed masked: `••••••Ab3c9` (only the last 5 characters visible). In the admin UI, keys are always displayed masked: `••••••Ab3c9` (only the last 5 characters visible).

View file

@ -74,6 +74,13 @@ class ProviderPage {
$clean['api_keys'][ $provider_id ] = $existing['api_keys'][ $provider_id ]; $clean['api_keys'][ $provider_id ] = $existing['api_keys'][ $provider_id ];
} }
} }
// Preserve DB-stored keys for providers whose UI field was disabled
// (wp-config.php constant override) and therefore never submitted.
foreach ( ( $existing['api_keys'] ?? array() ) as $provider_id => $stored ) {
if ( ! isset( $clean['api_keys'][ $provider_id ] ) ) {
$clean['api_keys'][ $provider_id ] = $stored;
}
}
$clean['models'] = array(); $clean['models'] = array();
foreach ( ( $input['models'] ?? array() ) as $provider_id => $model ) { foreach ( ( $input['models'] ?? array() ) as $provider_id => $model ) {

View file

@ -23,6 +23,12 @@ class SettingsPage {
*/ */
public const OPTION_KEY_SCHEMA = 'brezngeo_schema_settings'; public const OPTION_KEY_SCHEMA = 'brezngeo_schema_settings';
/**
* Provider IDs that support a wp-config.php constant override.
* A defined constant wins over the DB value and locks the UI field.
*/
private const CONSTANT_KEY_PROVIDERS = array( 'openai', 'anthropic', 'gemini', 'grok', 'openrouter' );
/** /**
* Returns merged settings from both option keys with defaults applied. * Returns merged settings from both option keys with defaults applied.
* Called by MetaGenerator, SchemaEnhancer, BulkPage, and admin pages. * Called by MetaGenerator, SchemaEnhancer, BulkPage, and admin pages.
@ -62,9 +68,37 @@ class SettingsPage {
$settings['api_keys'][ $id ] = $decrypted !== '' ? $decrypted : $stored; $settings['api_keys'][ $id ] = $decrypted !== '' ? $decrypted : $stored;
} }
// wp-config.php constants override the DB value and lock the admin field.
$settings['api_keys_locked'] = array();
foreach ( self::CONSTANT_KEY_PROVIDERS as $provider_id ) {
$constant = self::constantNameForProvider( $provider_id );
if ( $constant !== '' && defined( $constant ) ) {
$settings['api_keys'][ $provider_id ] = (string) constant( $constant );
$settings['api_keys_locked'][ $provider_id ] = true;
}
}
return $settings; return $settings;
} }
/**
* Returns the wp-config.php constant name for a given provider ID,
* or empty string if the provider has no constant override.
*
* Must be pure no WP hooks, no option reads. Called once per entry
* in CONSTANT_KEY_PROVIDERS during getSettings().
*
* The website (de/index.html, howto.html) already promises the names
* BREZNGEO_OPENAI_KEY, BREZNGEO_ANTHROPIC_KEY, BREZNGEO_GEMINI_KEY,
* BREZNGEO_GROK_KEY and BREZNGEO_OPENROUTER_KEY those are the contract.
*/
private static function constantNameForProvider( string $provider_id ): string {
if ( ! in_array( $provider_id, self::CONSTANT_KEY_PROVIDERS, true ) ) {
return '';
}
return 'BREZNGEO_' . strtoupper( $provider_id ) . '_KEY';
}
public static function getDefaultPrompt(): string { public static function getDefaultPrompt(): string {
$locale = get_locale(); $locale = get_locale();
$is_german = str_starts_with( $locale, 'de_' ); $is_german = str_starts_with( $locale, 'de_' );

View file

@ -9,31 +9,31 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
$or_models = $provider->getModels(); $brezngeo_or_models = $provider->getModels();
$or_saved_model = $settings['models']['openrouter'] ?? ''; $brezngeo_or_saved_model = $settings['models']['openrouter'] ?? '';
$or_is_custom = $or_saved_model !== '' && ! array_key_exists( $or_saved_model, $or_models ); $brezngeo_or_is_custom = $brezngeo_or_saved_model !== '' && ! array_key_exists( $brezngeo_or_saved_model, $brezngeo_or_models );
$or_cached_pricing = get_transient( \BreznGEO\Providers\OpenRouterProvider::MODELS_CACHE ); $brezngeo_or_cached_pricing = get_transient( \BreznGEO\Providers\OpenRouterProvider::MODELS_CACHE );
$or_cache_is_array = is_array( $or_cached_pricing ); $brezngeo_or_cache_is_array = is_array( $brezngeo_or_cached_pricing );
$or_selected_pricing = ( $or_cache_is_array && isset( $or_cached_pricing[ $or_saved_model ] ) ) $brezngeo_or_selected_pricing = ( $brezngeo_or_cache_is_array && isset( $brezngeo_or_cached_pricing[ $brezngeo_or_saved_model ] ) )
? $or_cached_pricing[ $or_saved_model ] ? $brezngeo_or_cached_pricing[ $brezngeo_or_saved_model ]
: null; : null;
?> ?>
<br><br> <br><br>
<label><?php esc_html_e( 'Model:', 'brezngeo' ); ?></label> <label><?php esc_html_e( 'Model:', 'brezngeo' ); ?></label>
<select name="brezngeo_settings[models][openrouter]" class="brezngeo-openrouter-model-select" id="brezngeo-openrouter-model"> <select name="brezngeo_settings[models][openrouter]" class="brezngeo-openrouter-model-select" id="brezngeo-openrouter-model">
<?php if ( empty( $or_models ) ) : ?> <?php if ( empty( $brezngeo_or_models ) ) : ?>
<option value=""><?php esc_html_e( 'No models loaded yet — click "Load models"', 'brezngeo' ); ?></option> <option value=""><?php esc_html_e( 'No models loaded yet — click "Load models"', 'brezngeo' ); ?></option>
<?php else : ?> <?php else : ?>
<?php foreach ( $or_models as $or_mid => $or_label ) : ?> <?php foreach ( $brezngeo_or_models as $brezngeo_or_mid => $brezngeo_or_label ) : ?>
<option value="<?php echo esc_attr( $or_mid ); ?>" <option value="<?php echo esc_attr( $brezngeo_or_mid ); ?>"
<?php selected( $or_saved_model, $or_mid ); ?> <?php selected( $brezngeo_or_saved_model, $brezngeo_or_mid ); ?>
data-input="<?php echo esc_attr( isset( $or_cached_pricing[ $or_mid ]['input_cost'] ) ? $or_cached_pricing[ $or_mid ]['input_cost'] : '' ); ?>" data-input="<?php echo esc_attr( isset( $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['input_cost'] ) ? $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['input_cost'] : '' ); ?>"
data-output="<?php echo esc_attr( isset( $or_cached_pricing[ $or_mid ]['output_cost'] ) ? $or_cached_pricing[ $or_mid ]['output_cost'] : '' ); ?>"> data-output="<?php echo esc_attr( isset( $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['output_cost'] ) ? $brezngeo_or_cached_pricing[ $brezngeo_or_mid ]['output_cost'] : '' ); ?>">
<?php echo esc_html( $or_label ); ?> <?php echo esc_html( $brezngeo_or_label ); ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
<option value="__custom__" <?php selected( $or_is_custom ); ?>> <option value="__custom__" <?php selected( $brezngeo_or_is_custom ); ?>>
<?php esc_html_e( 'Custom model ID…', 'brezngeo' ); ?> <?php esc_html_e( 'Custom model ID…', 'brezngeo' ); ?>
</option> </option>
</select> </select>
@ -42,14 +42,14 @@ $or_selected_pricing = ( $or_cache_is_array && isset( $or_cached_pricing[ $or_sa
</button> </button>
<span class="brezngeo-openrouter-load-status" aria-live="polite"></span> <span class="brezngeo-openrouter-load-status" aria-live="polite"></span>
<div class="brezngeo-openrouter-custom-wrap" style="<?php echo $or_is_custom ? '' : 'display:none;'; ?>margin-top:10px;"> <div class="brezngeo-openrouter-custom-wrap" style="<?php echo $brezngeo_or_is_custom ? '' : 'display:none;'; ?>margin-top:10px;">
<label for="brezngeo-openrouter-custom"> <label for="brezngeo-openrouter-custom">
<?php esc_html_e( 'Custom model ID:', 'brezngeo' ); ?> <?php esc_html_e( 'Custom model ID:', 'brezngeo' ); ?>
</label> </label>
<input type="text" <input type="text"
id="brezngeo-openrouter-custom" id="brezngeo-openrouter-custom"
name="brezngeo_settings[openrouter_custom_model]" name="brezngeo_settings[openrouter_custom_model]"
value="<?php echo esc_attr( $or_is_custom ? $or_saved_model : '' ); ?>" value="<?php echo esc_attr( $brezngeo_or_is_custom ? $brezngeo_or_saved_model : '' ); ?>"
placeholder="<?php esc_attr_e( 'e.g. anthropic/claude-opus-4.7', 'brezngeo' ); ?>" placeholder="<?php esc_attr_e( 'e.g. anthropic/claude-opus-4.7', 'brezngeo' ); ?>"
class="regular-text"> class="regular-text">
<p class="description"> <p class="description">
@ -69,11 +69,11 @@ $or_selected_pricing = ( $or_cache_is_array && isset( $or_cached_pricing[ $or_sa
<p style="margin-top:12px;"><strong><?php esc_html_e( 'Pricing (automatically from OpenRouter, per 1M tokens):', 'brezngeo' ); ?></strong></p> <p style="margin-top:12px;"><strong><?php esc_html_e( 'Pricing (automatically from OpenRouter, per 1M tokens):', 'brezngeo' ); ?></strong></p>
<div class="brezngeo-openrouter-pricing-display" id="brezngeo-openrouter-pricing" style="font-size:12px;color:#555;"> <div class="brezngeo-openrouter-pricing-display" id="brezngeo-openrouter-pricing" style="font-size:12px;color:#555;">
<?php if ( $or_selected_pricing ) : ?> <?php if ( $brezngeo_or_selected_pricing ) : ?>
Input $<span class="or-price-input"><?php echo esc_html( number_format( (float) $or_selected_pricing['input_cost'], 4 ) ); ?></span> Input $<span class="or-price-input"><?php echo esc_html( number_format( (float) $brezngeo_or_selected_pricing['input_cost'], 4 ) ); ?></span>
/ 1M · Output $<span class="or-price-output"><?php echo esc_html( number_format( (float) $or_selected_pricing['output_cost'], 4 ) ); ?></span> / 1M · Output $<span class="or-price-output"><?php echo esc_html( number_format( (float) $brezngeo_or_selected_pricing['output_cost'], 4 ) ); ?></span>
/ 1M / 1M
<?php elseif ( $or_is_custom ) : ?> <?php elseif ( $brezngeo_or_is_custom ) : ?>
<em><?php esc_html_e( 'Pricing unknown for custom models — will be populated after you click "Load models".', 'brezngeo' ); ?></em> <em><?php esc_html_e( 'Pricing unknown for custom models — will be populated after you click "Load models".', 'brezngeo' ); ?></em>
<?php else : ?> <?php else : ?>
<em><?php esc_html_e( 'Click "Load models" to fetch pricing from OpenRouter.', 'brezngeo' ); ?></em> <em><?php esc_html_e( 'Click "Load models" to fetch pricing from OpenRouter.', 'brezngeo' ); ?></em>

View file

@ -36,9 +36,26 @@
</td> </td>
</tr> </tr>
<?php foreach ( $providers as $id => $provider ) : // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?> <?php foreach ( $providers as $id => $provider ) : // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ?>
<?php $locked = ! empty( $settings['api_keys_locked'][ $id ] ); ?>
<tr class="brezngeo-provider-row" data-provider="<?php echo esc_attr( $id ); ?>"> <tr class="brezngeo-provider-row" data-provider="<?php echo esc_attr( $id ); ?>">
<th scope="row"><?php echo esc_html( $provider->getName() ); ?> <?php esc_html_e( 'API Key', 'brezngeo' ); ?></th> <th scope="row"><?php echo esc_html( $provider->getName() ); ?> <?php esc_html_e( 'API Key', 'brezngeo' ); ?></th>
<td> <td>
<?php if ( $locked ) : ?>
<span class="brezngeo-key-saved">
<?php
printf(
/* translators: %s: wp-config.php constant name */
esc_html__( 'Loaded from wp-config.php: %s', 'brezngeo' ),
'<code>BREZNGEO_' . esc_html( strtoupper( $id ) ) . '_KEY</code>'
);
?>
</span><br>
<input type="password" value="" placeholder="&#8212;" class="regular-text" disabled>
<button type="button" class="button brezngeo-test-btn" data-provider="<?php echo esc_attr( $id ); ?>">
<?php esc_html_e( 'Test connection', 'brezngeo' ); ?>
</button>
<span class="brezngeo-test-result" id="test-result-<?php echo esc_attr( $id ); ?>"></span>
<?php else : ?>
<?php if ( ! empty( $masked_keys[ $id ] ) ) : ?> <?php if ( ! empty( $masked_keys[ $id ] ) ) : ?>
<span class="brezngeo-key-saved"> <span class="brezngeo-key-saved">
<?php esc_html_e( 'Saved:', 'brezngeo' ); ?> <code><?php echo esc_html( $masked_keys[ $id ] ); ?></code> <?php esc_html_e( 'Saved:', 'brezngeo' ); ?> <code><?php echo esc_html( $masked_keys[ $id ] ); ?></code>
@ -54,6 +71,7 @@
<?php esc_html_e( 'Test connection', 'brezngeo' ); ?> <?php esc_html_e( 'Test connection', 'brezngeo' ); ?>
</button> </button>
<span class="brezngeo-test-result" id="test-result-<?php echo esc_attr( $id ); ?>"></span> <span class="brezngeo-test-result" id="test-result-<?php echo esc_attr( $id ); ?>"></span>
<?php endif; ?>
<?php if ( $id === 'openrouter' ) : ?> <?php if ( $id === 'openrouter' ) : ?>
<?php include BREZNGEO_DIR . 'includes/Admin/views/partials/openrouter-model-field.php'; ?> <?php include BREZNGEO_DIR . 'includes/Admin/views/partials/openrouter-model-field.php'; ?>
<?php else : ?> <?php else : ?>

View file

@ -237,6 +237,7 @@ No data is transmitted during normal page loads or to visitors.
* New: OpenRouter as a fifth AI provider — access to 600+ models (Claude, GPT, Gemini, Llama, Mistral, DeepSeek, and more) through a single API key. * New: OpenRouter as a fifth AI provider — access to 600+ models (Claude, GPT, Gemini, Llama, Mistral, DeepSeek, and more) through a single API key.
* New: On-demand model loader fetches OpenRouter's curated Marketing/SEO model list with live per-model pricing. * New: On-demand model loader fetches OpenRouter's curated Marketing/SEO model list with live per-model pricing.
* New: Custom model ID field lets you route to any OpenRouter model (e.g. anthropic/claude-opus-4.7) without waiting for a plugin update. * New: Custom model ID field lets you route to any OpenRouter model (e.g. anthropic/claude-opus-4.7) without waiting for a plugin update.
* New: OpenRouter joins the wp-config.php key-mapping (BREZNGEO_OPENROUTER_KEY) — define any provider key as a constant to keep it out of the database entirely; the admin field becomes read-only.
= 1.2.2 = = 1.2.2 =
* i18n: Added explicit load_plugin_textdomain() call for reliable translation loading on ClassicPress and other WordPress derivatives. * i18n: Added explicit load_plugin_textdomain() call for reliable translation loading on ClassicPress and other WordPress derivatives.