Dateien nach „bavarian-rank-engine/includes/Helpers“ hochladen

This commit is contained in:
Michael Fuchs 2026-02-22 10:07:20 +00:00
parent dd3110b93e
commit 438603951e
4 changed files with 262 additions and 0 deletions

View file

@ -0,0 +1,28 @@
<?php
namespace BavarianRankEngine\Helpers;
class BulkQueue {
private const LOCK_KEY = 'bre_bulk_running';
private const LOCK_TTL = 900; // 15 minutes
public static function acquire(): bool {
if ( self::isLocked() ) {
return false;
}
set_transient( self::LOCK_KEY, time(), self::LOCK_TTL );
return true;
}
public static function release(): void {
delete_transient( self::LOCK_KEY );
}
public static function isLocked(): bool {
return get_transient( self::LOCK_KEY ) !== false;
}
public static function lockAge(): int {
$started = get_transient( self::LOCK_KEY );
return $started !== false ? ( time() - (int) $started ) : 0;
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace BavarianRankEngine\Helpers;
class FallbackMeta {
private const MIN = 150;
private const MAX = 160;
/**
* Extract a clean 150160 char meta description from post content.
* Ends on a complete sentence or word boundary. No HTML.
*
* @param object $post Object with post_content property (WP_Post compatible)
*/
public static function extract( object $post ): string {
$text = wp_strip_all_tags( $post->post_content ?? '' );
$text = preg_replace( '/\s+/', ' ', $text );
$text = trim( $text );
if ( $text === '' ) {
return '';
}
if ( mb_strlen( $text ) <= self::MAX ) {
return $text;
}
// Try to end on a sentence boundary within MAX chars
$candidate = mb_substr( $text, 0, self::MAX );
$last_period = mb_strrpos( $candidate, '. ' );
$last_exclaim = mb_strrpos( $candidate, '! ' );
$last_question = mb_strrpos( $candidate, '? ' );
$last_sentence = max(
$last_period === false ? -1 : $last_period,
$last_exclaim === false ? -1 : $last_exclaim,
$last_question === false ? -1 : $last_question
);
if ( $last_sentence >= 0 && $last_sentence >= self::MIN - 1 ) {
return mb_substr( $text, 0, $last_sentence + 1 );
}
// Fall back to last word boundary within MAX
$last_space = mb_strrpos( $candidate, ' ' );
if ( $last_space !== false && $last_space >= self::MIN - 20 ) {
return mb_substr( $text, 0, $last_space ) . '…';
}
// Last resort: hard cut with ellipsis
return mb_substr( $text, 0, self::MAX - 1 ) . '…';
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace BavarianRankEngine\Helpers;
/**
* Obfuscates API keys for database storage using XOR with a derived WP-salt key.
*
* No OpenSSL or other PHP extensions required only core string functions.
* Keys stored as "bre1:<base64(xor(plaintext, salt))>".
*
* Note: XOR with a static salt is obfuscation, not encryption. It prevents
* plain-text keys from appearing in database backups or export files, but
* does not protect against an attacker with access to both the database
* AND the wp-config.php salts. For stronger protection, users can define
* BRE_<PROVIDER>_KEY constants in wp-config.php and leave the DB field empty.
*/
class KeyVault {
private const PREFIX = 'bre1:';
/**
* Obfuscate a plain API key for database storage.
*/
public static function encrypt( string $key ): string {
if ( $key === '' ) {
return '';
}
return self::PREFIX . base64_encode( self::xor( $key, self::salt() ) );
}
/**
* Recover the plain API key from a stored obfuscated value.
* Returns empty string if the stored value is not in bre1: format (legacy/invalid).
*/
public static function decrypt( string $stored ): string {
if ( $stored === '' ) {
return '';
}
if ( ! str_starts_with( $stored, self::PREFIX ) ) {
// Legacy OpenSSL-encrypted value or unknown format — return empty so user re-enters.
return '';
}
$raw = base64_decode( substr( $stored, strlen( self::PREFIX ) ), true );
if ( $raw === false ) {
return '';
}
return self::xor( $raw, self::salt() );
}
/**
* Returns masked version for display: ••••••Ab3c9
*/
public static function mask( string $plain ): string {
if ( $plain === '' ) {
return '';
}
return str_repeat( '•', max( 0, mb_strlen( $plain ) - 5 ) ) . mb_substr( $plain, -5 );
}
/**
* XOR each byte of $data with the corresponding byte of $key (wrapping).
*/
private static function xor( string $data, string $key ): string {
$out = '';
$keyLen = strlen( $key );
for ( $i = 0, $n = strlen( $data ); $i < $n; $i++ ) {
$out .= $data[ $i ] ^ $key[ $i % $keyLen ];
}
return $out;
}
/**
* Derives a 64-character hex salt from WP's AUTH_KEY and SECURE_AUTH_KEY.
* Falls back to known strings if the constants are not defined (local dev / unit tests).
*/
private static function salt(): string {
$a = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'bre-fallback-a';
$b = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : 'bre-fallback-b';
return hash( 'sha256', $a . $b ); // 64 hex chars, no extension needed
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace BavarianRankEngine\Helpers;
class TokenEstimator {
/**
* Pricing per 1k tokens [provider][model][input|output]
* Update these when provider pricing changes.
*/
private const PRICING = array(
'openai' => array(
'gpt-4.1' => array(
'input' => 0.002,
'output' => 0.008,
),
'gpt-4o' => array(
'input' => 0.0025,
'output' => 0.01,
),
'gpt-4o-mini' => array(
'input' => 0.00015,
'output' => 0.0006,
),
'gpt-3.5-turbo' => array(
'input' => 0.0005,
'output' => 0.0015,
),
),
'anthropic' => array(
'claude-sonnet-4-6' => array(
'input' => 0.003,
'output' => 0.015,
),
'claude-opus-4-6' => array(
'input' => 0.015,
'output' => 0.075,
),
'claude-haiku-4-5-20251001' => array(
'input' => 0.00025,
'output' => 0.00125,
),
),
'gemini' => array(
'gemini-2.0-flash' => array(
'input' => 0.00010,
'output' => 0.00040,
),
'gemini-2.0-flash-lite' => array(
'input' => 0.000038,
'output' => 0.00015,
),
'gemini-1.5-pro' => array(
'input' => 0.00125,
'output' => 0.005,
),
),
'grok' => array(
'grok-3' => array(
'input' => 0.003,
'output' => 0.015,
),
'grok-3-mini' => array(
'input' => 0.0003,
'output' => 0.0005,
),
),
);
/** Estimate token count (~4 chars per token) */
public static function estimate( string $text ): int {
return (int) ceil( mb_strlen( $text ) / 4 );
}
/** Truncate text to approximately $max_tokens */
public static function truncate( string $text, int $max_tokens ): string {
$max_chars = $max_tokens * 4;
if ( mb_strlen( $text ) <= $max_chars ) {
return $text;
}
return mb_substr( $text, 0, $max_chars );
}
/**
* Estimate cost in USD.
*
* @param int $tokens Number of tokens
* @param string $provider Provider ID
* @param string $model Model ID
* @param string $type 'input' or 'output'
*/
public static function estimateCost( int $tokens, string $provider, string $model, string $type = 'input' ): float {
$price_per_1k = self::PRICING[ $provider ][ $model ][ $type ] ?? 0.002;
return round( ( $tokens / 1000 ) * $price_per_1k, 6 );
}
/** Human-readable cost string e.g. "~0,05 €" */
public static function formatCost( float $usd ): string {
$eur = $usd * 0.92;
if ( $eur < 0.01 ) {
return '< 0,01 €';
}
return '~' . number_format( $eur, 2, ',', '.' ) . ' €';
}
}