Files
peptide-starter-theme/template-parts/settings-panel.php
peptiderepo cb8b73f4ab v1.5.1: frontend auth hardening — rate limiting, email verification, honeypots, CSV safety
Security-only release per ADR-0001. Closes all six ship-blockers from the
post-v1.5.0 audit: PSEC-001 (no rate limiting), PSEC-002 (account
enumeration via separate error messages), PSEC-003 (registration auto-login
with no email verification), PSEC-004 (ungated "My Data" templates),
PSEC-005 (CSV formula injection on subscriber export), PSEC-006 (no
honeypot/CAPTCHA on public forms).

Adds inc/config.php (single source of truth for all security thresholds,
filterable via peptide_starter_security_config), inc/rate-limiter.php
(Peptide_Starter_Rate_Limiter + Cloudflare-aware client IP resolver,
transient-backed, never stores raw IPs), and inc/email-verification.php
(43-char token, 24h TTL, /verify rewrite, rate-limited resend endpoint).

Auth handlers rewritten to: unify all failure messages, rate-limit before
any expensive check, drop registrations with a filled honeypot silently,
stop auto-login on register, send a verification email instead. The
require_login() helper lives in inc/helpers.php and is applied at the top
of page-tracker.php, page-subject-log.php, and page-protocol-builder.php;
page-calculator.php stays open (no PII, no writes).

Newsletter signup gains: consent checkbox, privacy link, honeypot,
autoload=false on the wp_options blob, unsubscribe token stored per row,
/newsletter-unsubscribe route, merged success+duplicate into a single "ok"
state. Admin export runs every cell through peptide_starter_csv_safe()
which prefixes leading =/+/-/@/tab/CR with a quote.

Contact handler hardens against email-header injection (rejects name
values with \r \n , < >), rate-limits, honors honeypot, tags mail
failures with a request ID for correlation without logging message
content.

Adds Tools → Mail Test (inc/mail-diagnostic.php): a permanent wp_mail
probe so we can check Hostinger outbound deliverability any time it looks
flaky. Adds a one-time admin_init migration that enrols pre-v1.5.1
subscribers into the verification flow (currently a no-op — zero users —
but guarded for the future).

Other cleanup from the audit: fallback primary menu moved out of
header.php into inc/helpers.php (redeclaration risk), placeholder footer
language links replaced with "Translations coming soon", footer copyright
now reads through the customizer getter, settings-panel gains a proper
Tab/Shift+Tab focus trap and saves/restores body overflow, documentation
heading IDs slugified from heading text with collision counters, JS files
standardized on const/let.

Six new PHPUnit suites cover rate limiter lifecycle, auth handler error
unification and rate limits, email verification token generation and
expiry, contact handler header injection + rate limit + honeypot,
newsletter signup consent + CSV safety, and require_login gate behavior.

ARCHITECTURE.md and CONVENTIONS.md updated in the same commit — new
module layout, data flows, security patterns for rate-limiting,
honeypotting, gating, and CSV-safe export.

Version bumped to 1.5.1 in style.css and functions.php.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 13:01:22 +07:00

104 lines
4.6 KiB
PHP

<?php
/**
* Template Part: Settings Panel
*
* Slide-out panel triggered by the settings icon in the header.
* Contains contact form, compound data submission link, and support info.
*
* @see assets/js/settings-panel.js — open/close toggle
* @see functions.php — AJAX handler for contact form
* @see header.php — settings icon trigger
*
* What: Slide-out support panel with contact form and links.
* Who calls it: get_template_part( 'template-parts/settings', 'panel' ) in footer.php.
* Dependencies: None — self-contained HTML.
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$current_user = wp_get_current_user();
$user_name = is_user_logged_in() ? $current_user->display_name : '';
$user_email = is_user_logged_in() ? $current_user->user_email : '';
?>
<!-- Settings Panel Overlay -->
<div class="ps-settings-overlay" id="ps-settings-overlay" aria-hidden="true"></div>
<!-- Settings Panel -->
<aside class="ps-settings-panel" id="ps-settings-panel" aria-hidden="true" aria-label="<?php esc_attr_e( 'Support panel', 'peptide-starter' ); ?>">
<div class="ps-settings-panel__header">
<h2><?php esc_html_e( 'Support', 'peptide-starter' ); ?></h2>
<button class="ps-settings-panel__close" id="ps-settings-close" aria-label="<?php esc_attr_e( 'Close panel', 'peptide-starter' ); ?>">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M15 5L5 15M5 5L15 15"/>
</svg>
</button>
</div>
<div class="ps-settings-panel__body">
<!-- Contact Engineering Form -->
<div class="ps-settings-section">
<h3><?php esc_html_e( 'Contact Engineering', 'peptide-starter' ); ?></h3>
<form id="ps-contact-form" class="ps-contact-form" novalidate>
<?php wp_nonce_field( 'ps_contact_form', 'ps_contact_nonce' ); ?>
<input type="hidden" name="action" value="ps_contact_submit">
<?php peptide_starter_render_honeypot( 'contact' ); ?>
<div class="ps-form-group">
<label for="ps-contact-name"><?php esc_html_e( 'Name', 'peptide-starter' ); ?></label>
<input type="text" id="ps-contact-name" name="name" value="<?php echo esc_attr( $user_name ); ?>" required>
</div>
<div class="ps-form-group">
<label for="ps-contact-email"><?php esc_html_e( 'Email', 'peptide-starter' ); ?></label>
<input type="email" id="ps-contact-email" name="email" value="<?php echo esc_attr( $user_email ); ?>" required>
</div>
<div class="ps-form-group">
<label for="ps-contact-topic"><?php esc_html_e( 'Topic', 'peptide-starter' ); ?></label>
<select id="ps-contact-topic" name="topic" required>
<option value=""><?php esc_html_e( 'Select a topic', 'peptide-starter' ); ?></option>
<option value="bug"><?php esc_html_e( 'Bug Report', 'peptide-starter' ); ?></option>
<option value="feature"><?php esc_html_e( 'Feature Request', 'peptide-starter' ); ?></option>
<option value="data"><?php esc_html_e( 'Data Correction', 'peptide-starter' ); ?></option>
<option value="other"><?php esc_html_e( 'Other', 'peptide-starter' ); ?></option>
</select>
</div>
<div class="ps-form-group">
<label for="ps-contact-message"><?php esc_html_e( 'Description', 'peptide-starter' ); ?></label>
<textarea id="ps-contact-message" name="message" rows="4" required></textarea>
</div>
<button type="submit" class="ps-btn ps-btn-primary ps-btn-full">
<?php esc_html_e( 'Send Message', 'peptide-starter' ); ?>
</button>
<div id="ps-contact-status" class="ps-contact-status" aria-live="polite"></div>
</form>
</div>
<!-- Submit Compound Data -->
<div class="ps-settings-section">
<h3><?php esc_html_e( 'Submit Compound Data', 'peptide-starter' ); ?></h3>
<p><?php esc_html_e( 'Have data on a peptide not in our directory? Submit it for review.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/compound-request' ) ); ?>" class="ps-btn ps-btn-secondary ps-btn-full">
<?php esc_html_e( 'Submit Compound', 'peptide-starter' ); ?>
</a>
</div>
<!-- Support Policy -->
<div class="ps-settings-section ps-settings-disclaimer">
<h3><?php esc_html_e( 'Support Policy', 'peptide-starter' ); ?></h3>
<p>
<?php esc_html_e( 'Peptide Repo is a research reference database only. All data is provided for informational and educational purposes. This platform does not provide medical advice, diagnoses, or treatment recommendations. Always consult qualified professionals for clinical decisions.', 'peptide-starter' ); ?>
</p>
</div>
</div>
</aside>