- Implemented across bootstrap.php, chat frontend, and dashboard:

Per-tier audio bitrate: Free 64, Plus 96, Pro 128 kbps.
Configurable text/voice reply threads and depth.
Registration/login-only honeypot and internal CAPTCHA.
Optional Google reCAPTCHA v2/v3 with server verification.
Archive/export support for replies and bitrate.
Dashboard controls for all settings.
Verified JavaScript, JSON, SQLite migrations, desktop/mobile UI, and 
browser console. Native PHP lint was unavailable because PHP is not 
installed.
Google implementation follows Siteverify and reCAPTCHA v3.
This commit is contained in:
Ty Clifford
2026-06-09 10:59:58 -04:00
parent 88805057cc
commit eb57cde99f
14 changed files with 844 additions and 110 deletions
+94 -3
View File
@@ -4,8 +4,33 @@ function handleAdminPlatformAction(string $action): ?string {
if ($action === 'save_platform_services') {
$voiceProfile = (string)($_POST['voice_transcoding_profile'] ?? 'm4a_aac');
if (!in_array($voiceProfile, ['m4a_aac', 'mp4_h264_aac'], true)) $voiceProfile = 'm4a_aac';
$captchaVersion = (string)($_POST['captcha_google_version'] ?? 'v2');
if (!in_array($captchaVersion, ['v2', 'v3'], true)) $captchaVersion = 'v2';
$captchaDifficulty = (string)($_POST['captcha_internal_difficulty'] ?? 'easy');
if (!in_array($captchaDifficulty, ['easy', 'medium'], true)) $captchaDifficulty = 'easy';
$captchaGoogleEnabled = !empty($_POST['captcha_google_enabled']);
$captchaGoogleSiteKey = trim((string)($_POST['captcha_google_site_key'] ?? ''));
$captchaGoogleSecretKey = trim((string)($_POST['captcha_google_secret_key'] ?? ''));
if ($captchaGoogleEnabled && ($captchaGoogleSiteKey === '' || $captchaGoogleSecretKey === '')) {
throw new RuntimeException('Google reCAPTCHA requires both a site key and secret key.');
}
setConfigValues([
'subscriptions.enabled' => !empty($_POST['subscriptions_enabled']),
'chat.replies_enabled' => !empty($_POST['replies_enabled']),
'chat.reply_max_depth' => max(1, min(20, (int)($_POST['reply_max_depth'] ?? 4))),
'security.captcha.registration_enabled' => !empty($_POST['captcha_registration_enabled']),
'security.captcha.login_enabled' => !empty($_POST['captcha_login_enabled']),
'security.captcha.honeypot.enabled' => !empty($_POST['captcha_honeypot_enabled']),
'security.captcha.internal.enabled' => !empty($_POST['captcha_internal_enabled']),
'security.captcha.internal.difficulty' => $captchaDifficulty,
'security.captcha.internal.ttl_seconds' => max(60, min(1800, (int)($_POST['captcha_internal_ttl'] ?? 300))),
'security.captcha.internal.max_attempts' => max(1, min(10, (int)($_POST['captcha_internal_attempts'] ?? 3))),
'security.captcha.google.enabled' => $captchaGoogleEnabled,
'security.captcha.google.version' => $captchaVersion,
'security.captcha.google.site_key' => $captchaGoogleSiteKey,
'security.captcha.google.secret_key' => $captchaGoogleSecretKey,
'security.captcha.google.v3_min_score' => max(0.0, min(1.0, (float)($_POST['captcha_google_v3_score'] ?? 0.5))),
'security.captcha.google.hostname' => trim((string)($_POST['captcha_google_hostname'] ?? '')),
'mailgun.api_key' => trim((string)($_POST['mailgun_api_key'] ?? '')),
'mailgun.domain' => trim((string)($_POST['mailgun_domain'] ?? '')),
'mailgun.from' => trim((string)($_POST['mailgun_from'] ?? '')),
@@ -64,6 +89,9 @@ function handleAdminPlatformAction(string $action): ?string {
foreach ($boolFeatures as $key) {
$upsertFeature->execute([$id, $key, json_encode(!empty($plan['features'][$key]))]);
}
$upsertFeature->execute([$id, 'voice_bitrate_kbps', json_encode(
max(48, min(320, (int)($plan['features']['voice_bitrate_kbps'] ?? 64)))
)]);
$upsertFeature->execute([$id, 'text_archive_days', json_encode(max(30, min(3650, (int)($plan['features']['text_archive_days'] ?? 30))))]);
}
$db->commit();
@@ -87,7 +115,8 @@ function handleAdminPlatformAction(string $action): ?string {
->execute([$slug, $name, trim((string)($_POST['description'] ?? '')), time(), time()]);
$id = (int)$db->lastInsertId();
$features = [
'voice_messages' => true, 'recording_status' => false, 'auto_voice_send' => false,
'voice_messages' => true, 'voice_bitrate_kbps' => 64,
'recording_status' => false, 'auto_voice_send' => false,
'auto_voice_play' => false, 'username_color' => false, 'text_archive_days' => 30,
'text_export' => true, 'voice_archive' => false, 'voice_export' => false, 'email_2fa' => false,
];
@@ -124,7 +153,7 @@ function renderAdminPlatformTab(): void {
$events = $db->query("SELECT event_type,COUNT(*) AS total FROM visitor_events
WHERE created_at>" . (time() - 2592000) . " GROUP BY event_type ORDER BY total DESC")->fetchAll();
$featureKeys = [
'voice_messages', 'recording_status', 'auto_voice_send', 'auto_voice_play',
'voice_messages', 'voice_bitrate_kbps', 'recording_status', 'auto_voice_send', 'auto_voice_play',
'username_color', 'text_archive_days', 'text_export', 'voice_archive', 'voice_export', 'email_2fa',
];
?>
@@ -144,6 +173,64 @@ function renderAdminPlatformTab(): void {
<div class="panel-body"><label class="cb-row"><input type="checkbox" name="subscriptions_enabled" <?= getConfigVal('subscriptions.enabled', true) ? 'checked' : '' ?>>
<span>Allow and display new subscriptions. When off, existing subscribers can still manage billing.</span></label></div></div>
<div class="panel"><div class="panel-head"><span class="panel-title">// REPLY THREADS</span></div><div class="panel-body">
<div class="form-row">
<label class="cb-row"><input type="checkbox" name="replies_enabled" <?= getConfigVal('chat.replies_enabled', true) ? 'checked' : '' ?>>
<span>Enable reply-to controls for text and voice messages</span></label>
<div class="field"><label>Maximum Reply Depth</label><input type="number" min="1" max="20"
name="reply_max_depth" value="<?= (int)getConfigVal('chat.reply_max_depth', 4) ?>"></div>
</div>
</div></div>
<div class="panel"><div class="panel-head"><span class="panel-title">// REGISTRATION / LOGIN CAPTCHA</span></div><div class="panel-body">
<p class="dim">CAPTCHA checks apply only to initial registration and password login. Email 2FA verification is not challenged.</p>
<div class="form-row">
<label class="cb-row"><input type="checkbox" name="captcha_registration_enabled"
<?= getConfigVal('security.captcha.registration_enabled', true) ? 'checked' : '' ?>><span>Protect registration</span></label>
<label class="cb-row"><input type="checkbox" name="captcha_login_enabled"
<?= getConfigVal('security.captcha.login_enabled', true) ? 'checked' : '' ?>><span>Protect login</span></label>
</div>
<div class="form-row">
<label class="cb-row"><input type="checkbox" name="captcha_honeypot_enabled"
<?= getConfigVal('security.captcha.honeypot.enabled', true) ? 'checked' : '' ?>>
<span>Invisible honeypot fields that deny bot-like submissions</span></label>
<label class="cb-row"><input type="checkbox" name="captcha_internal_enabled"
<?= getConfigVal('security.captcha.internal.enabled', true) ? 'checked' : '' ?>>
<span>Internal arithmetic challenge</span></label>
</div>
<div class="form-row3">
<div class="field"><label>Internal Difficulty</label><select name="captcha_internal_difficulty">
<?php $captchaDifficulty = getConfigVal('security.captcha.internal.difficulty', 'easy'); ?>
<option value="easy" <?= $captchaDifficulty === 'easy' ? 'selected' : '' ?>>Easy (1-9)</option>
<option value="medium" <?= $captchaDifficulty === 'medium' ? 'selected' : '' ?>>Medium (1-25)</option>
</select></div>
<div class="field"><label>Challenge TTL (seconds)</label><input type="number" min="60" max="1800"
name="captcha_internal_ttl" value="<?= (int)getConfigVal('security.captcha.internal.ttl_seconds', 300) ?>"></div>
<div class="field"><label>Maximum Attempts</label><input type="number" min="1" max="10"
name="captcha_internal_attempts" value="<?= (int)getConfigVal('security.captcha.internal.max_attempts', 3) ?>"></div>
</div>
<label class="cb-row"><input type="checkbox" name="captcha_google_enabled"
<?= getConfigVal('security.captcha.google.enabled', false) ? 'checked' : '' ?>>
<span>Also require Google reCAPTCHA</span></label>
<div class="form-row3">
<div class="field"><label>Google Version</label><select name="captcha_google_version">
<?php $captchaVersion = getConfigVal('security.captcha.google.version', 'v2'); ?>
<option value="v2" <?= $captchaVersion === 'v2' ? 'selected' : '' ?>>v2 checkbox</option>
<option value="v3" <?= $captchaVersion === 'v3' ? 'selected' : '' ?>>v3 score</option>
</select></div>
<div class="field"><label>Site Key</label><input name="captcha_google_site_key"
value="<?= esc(getConfigVal('security.captcha.google.site_key', '')) ?>"></div>
<div class="field"><label>Secret Key</label><input type="password" name="captcha_google_secret_key"
value="<?= esc(getConfigVal('security.captcha.google.secret_key', '')) ?>"></div>
</div>
<div class="form-row">
<div class="field"><label>v3 Minimum Score</label><input type="number" min="0" max="1" step="0.1"
name="captcha_google_v3_score" value="<?= esc(getConfigVal('security.captcha.google.v3_min_score', 0.5)) ?>"></div>
<div class="field"><label>Expected Hostname (optional)</label><input name="captcha_google_hostname"
value="<?= esc(getConfigVal('security.captcha.google.hostname', '')) ?>" placeholder="chat.example.com"></div>
</div>
</div></div>
<div class="panel"><div class="panel-head"><span class="panel-title">// MAILGUN</span></div><div class="panel-body">
<div class="form-row3">
<div class="field"><label>API Key</label><input type="password" name="mailgun_api_key" value="<?= esc(getConfigVal('mailgun.api_key', '')) ?>"></div>
@@ -178,7 +265,7 @@ function renderAdminPlatformTab(): void {
<option value="m4a_aac" <?= $voiceProfile === 'm4a_aac' ? 'selected' : '' ?>>M4A / AAC-LC (recommended)</option>
<option value="mp4_h264_aac" <?= $voiceProfile === 'mp4_h264_aac' ? 'selected' : '' ?>>MP4 / H.264 + AAC</option>
</select></div>
<div class="field"><label>Audio Bitrate (kbps)</label><input type="number" min="48" max="320"
<div class="field"><label>Fallback Audio Bitrate (kbps)</label><input type="number" min="48" max="320"
name="voice_audio_bitrate" value="<?= (int)getConfigVal('voice.transcoding.audio_bitrate_kbps', 128) ?>"></div>
</div>
<div class="form-row">
@@ -230,6 +317,10 @@ function renderAdminPlatformTab(): void {
<div class="field"><label><?= esc(adminFeatureLabel($key)) ?></label><input type="number"
min="30"
name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]" value="<?= (int)($plan['features'][$key] ?? 0) ?>"></div>
<?php elseif ($key === 'voice_bitrate_kbps'): ?>
<div class="field"><label><?= esc(adminFeatureLabel($key)) ?></label><input type="number"
min="48" max="320"
name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]" value="<?= (int)($plan['features'][$key] ?? 64) ?>"></div>
<?php else: ?>
<label class="cb-row"><input type="checkbox" name="plans[<?= $plan['id'] ?>][features][<?= $key ?>]"
<?= !empty($plan['features'][$key]) ? 'checked' : '' ?>><span><?= esc(adminFeatureLabel($key)) ?></span></label>
+4 -2
View File
@@ -72,14 +72,16 @@ function runVoiceTranscodeCommand(array $command, int $timeoutSeconds): array {
function transcodeVoiceRecording(
string $sourcePath,
string $sourceFilename,
string $sourceMime
string $sourceMime,
?int $bitrateOverride = null
): array {
if (!getConfigVal('voice.transcoding.enabled', false)) {
return ['filename' => $sourceFilename, 'mime' => $sourceMime, 'transcoded' => false];
}
$profile = voiceTranscodingProfile();
$bitrate = max(48, min(320, (int)getConfigVal('voice.transcoding.audio_bitrate_kbps', 128)));
$bitrate = max(48, min(320, $bitrateOverride
?? (int)getConfigVal('voice.transcoding.audio_bitrate_kbps', 128)));
$timeout = max(5, min(300, (int)getConfigVal('voice.transcoding.timeout_seconds', 45)));
$fallback = (bool)getConfigVal('voice.transcoding.fallback_to_source', true);
$token = pathinfo($sourceFilename, PATHINFO_FILENAME);