docs(08): research phase domain

Phase 08: Bug Fixes & Legacy Parity
- Session bug fix pattern identified (hidden field + uniqid)
- Additional work sections integration patterns documented
- Sonstiges field implementation researched
This commit is contained in:
2026-02-06 22:44:34 +09:00
parent ab36dca53c
commit c0df7c5cdf

View File

@@ -0,0 +1,379 @@
# Phase 8: Bug Fixes & Legacy Parity - Research
**Researched:** 2026-02-06
**Domain:** WordPress form error handling, data integration, legacy feature parity
**Confidence:** HIGH
## Summary
This phase addresses three gaps identified in the v1.0 milestone audit: (1) session_id() bug causing error message cross-contamination, (2) orphaned additional work sections data that was extracted but never integrated, and (3) missing Sonstiges free text field from legacy form.
All three issues are internal to the plugin with clear solutions based on existing code patterns. The session bug requires replacing PHP session_id() with WordPress-native unique identifiers. The additional work sections require extending existing form renderer, email generator, form handler, and validation patterns to accommodate checkbox/text field types beyond the furniture inventory's quantity-based structure. Sonstiges is a simple textarea field addition.
**Primary recommendation:** Fix session bug first (critical path blocking production), then integrate additional work sections following established room section patterns, then add Sonstiges field last (simplest).
## Standard Stack
The phase uses only existing WordPress and plugin infrastructure:
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| WordPress | 6.x | Transient API, nonces, sanitization | Only dependency, already in use |
| PHP | 7.4+ | uniqid(), random_int() | Built-in unique ID generation |
| jQuery | 1.12+ (WP bundled) | Form field event handling | Already loaded by Phase 5 |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| None | - | - | All needs met by core WordPress |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| uniqid() | wp_generate_password() | Password generator is cryptographically random but slower, unnecessary for transient keys |
| Hidden field | Cookie-based tracking | Cookies require consent, hidden field is simpler and privacy-friendly |
| Transients | User meta | User meta requires logged-in users, transients work for anonymous form submissions |
**Installation:**
No new dependencies required.
## Architecture Patterns
### Session Bug Fix Pattern: Hidden Field + Transient
**Current (broken):**
```php
// form-handler.php line 72, 82
set_transient( 'umzugsliste_errors_' . session_id(), $errors, 300 );
// form-renderer.php line 49
$session_id = session_id(); // Returns empty string without session_start()
$errors = get_transient( 'umzugsliste_errors_' . $session_id );
```
**Problem:** session_id() returns empty string when PHP sessions not initialized, degrading to `umzugsliste_errors_default` shared across all users.
**WordPress-native solution:**
```php
// In form renderer - generate unique ID and embed in form
$form_id = 'umzugsliste_' . uniqid( '', true );
echo '<input type="hidden" name="umzugsliste_form_id" value="' . esc_attr( $form_id ) . '">';
// In form handler - retrieve the same ID from POST
$form_id = sanitize_text_field( $_POST['umzugsliste_form_id'] ?? '' );
if ( empty( $form_id ) ) {
$form_id = 'umzugsliste_' . uniqid( '', true ); // Fallback for edge cases
}
set_transient( 'umzugsliste_errors_' . $form_id, $errors, 300 );
wp_safe_redirect( add_query_arg( 'form_id', $form_id, wp_get_referer() ) );
// Back in renderer - check both POST and GET for form_id
$form_id = '';
if ( isset( $_POST['umzugsliste_form_id'] ) ) {
$form_id = sanitize_text_field( $_POST['umzugsliste_form_id'] );
} elseif ( isset( $_GET['form_id'] ) ) {
$form_id = sanitize_text_field( $_GET['form_id'] );
}
if ( ! empty( $form_id ) ) {
$errors = get_transient( 'umzugsliste_errors_' . $form_id );
delete_transient( 'umzugsliste_errors_' . $form_id );
}
```
**Why this works:**
- uniqid() generates unique ID based on current time in microseconds
- Hidden field passes ID through form submission (survives redirect)
- Query parameter passes ID back after redirect
- Transient uses unique key per form instance
- 300-second expiration prevents database bloat
- No sessions, no cookies, no privacy concerns
**Source:** [Creating unique transient name per browser session | WordPress.org](https://wordpress.org/support/topic/creating-unique-transient-name-per-browser-session/)
### Additional Work Sections Integration Pattern
**Data structure (already exists in class-furniture-data.php lines 230-295):**
```php
public static function get_additional_work() {
return array(
'montage' => array(
'label' => 'Montagearbeiten',
'fields' => array(
array( 'name' => 'Montagearbeiten fallen nicht an', 'type' => 'checkbox' ),
array( 'name' => 'Ich habe spezielle Montagewünsche', 'type' => 'checkbox' ),
),
),
'schrank' => array(
'label' => 'Schrank',
'fields' => array(
array( 'name' => 'Schrankwand', 'type' => 'abbau_aufbau' ),
// ... 5 more items
),
),
// ... 4 more sections: elektriker, duebelarbeiten, packarbeiten, anfahrt
);
}
```
**Field types found:**
- `checkbox` - Single yes/no option
- `abbau_aufbau` - Radio group with Abbau/Aufbau/Beides options (inferred from legacy)
- `checkbox_anzahl` - Checkbox + text input for quantity
- `text` - Simple text input
**Form rendering pattern (extend render_all_rooms approach):**
```php
// In class-form-renderer.php, after render_all_rooms() in render() method
self::render_additional_work_sections();
private static function render_additional_work_sections() {
$sections = Umzugsliste_Furniture_Data::get_additional_work();
foreach ( $sections as $section_key => $section_data ) {
self::render_additional_work_section( $section_key, $section_data );
}
}
private static function render_additional_work_section( $section_key, $section_data ) {
// Panel header matching room sections (line 249 pattern)
// Table with 2 columns (no cbm calculations)
// Iterate through fields, render based on type
// Field names: info[{section_key}_{field_name}] or custom key if specified
}
```
**Email generation pattern (extend generate_all_rooms approach):**
```php
// In class-email-generator.php, after generate_all_rooms() in generate() method
$content .= self::generate_additional_work_sections( $data );
private static function generate_additional_work_sections( $data ) {
$html = '';
$sections = Umzugsliste_Furniture_Data::get_additional_work();
foreach ( $sections as $section_key => $section_data ) {
// Only include section if it has data
if ( self::has_additional_work_data( $data, $section_key ) ) {
$html .= self::generate_additional_work_section( $section_key, $section_data, $data );
}
}
return $html;
}
// Table structure matching room tables (line 184 pattern)
// No cbm columns, just field name and value
```
**Validation pattern (extend validate_submission):**
```php
// In class-form-handler.php validate_submission() method
// No required fields in additional work sections (all optional)
// Sanitize checkbox values (on/off), text inputs (sanitize_text_field)
// No complex validation needed
```
**Sanitization pattern (extend sanitize_submission):**
```php
// In class-form-handler.php sanitize_submission() method
// Additional work fields come through $_POST['info'] array
// Already being sanitized in lines 204-213
// Ensure all additional work field keys are included
```
### Sonstiges Free Text Field Pattern
**Simplest addition - single textarea:**
```php
// In class-form-renderer.php, before render_grand_totals()
self::render_sonstiges_field();
private static function render_sonstiges_field() {
?>
<div class="row">
<div class="large-12 columns">
<div class="panel">
<h3>Sonstiges</h3>
</div>
<div class="large-12 columns">
<label for="sonstiges">Weitere Anmerkungen:</label>
<textarea name="info[sonstiges]" id="sonstiges" rows="5"></textarea>
</div>
</div>
</div>
<?php
}
```
**Email inclusion:**
```php
// In class-email-generator.php, after additional work sections
if ( ! empty( $data['info']['sonstiges'] ) ) {
$content .= self::generate_sonstiges_section( $data['info']['sonstiges'] );
}
private static function generate_sonstiges_section( $text ) {
return "<div class='row'>
<div class='large-12 columns'>
<h4>Sonstiges</h4>
<p>" . nl2br( esc_html( $text ) ) . "</p>
</div>
</div>";
}
```
## Don't Hand-Roll
Problems that look simple but have existing solutions:
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Unique session tracking | PHP sessions | Hidden field + uniqid() + transients | Many WordPress hosts disable sessions, transients are WordPress-native |
| Form field rendering | Manual HTML strings | Existing render patterns | Consistency with room sections, easier to maintain |
| Data sanitization | Custom regex/filters | sanitize_text_field(), sanitize_textarea_field() | WordPress core functions handle edge cases |
**Key insight:** WordPress provides native solutions for all requirements. Session-based approaches fail in many hosting environments due to disabled PHP sessions or aggressive caching.
## Common Pitfalls
### Pitfall 1: Assuming PHP Sessions Work
**What goes wrong:** session_id() returns empty string, all users share same transient key, errors cross-contaminate between simultaneous submissions
**Why it happens:** Many WordPress hosts disable PHP sessions for performance/security, caching plugins bypass session_start()
**How to avoid:** Use hidden field + GET parameter pattern, test without calling session_start()
**Warning signs:** Transient key is 'umzugsliste_errors_default', errors appear/disappear randomly
### Pitfall 2: Forgetting to Delete Transients After Display
**What goes wrong:** Error messages persist across multiple page loads, old errors shown on new submissions
**Why it happens:** Transients have 300-second lifetime, deleting after display prevents reuse
**How to avoid:** Always delete_transient() immediately after get_transient() when displaying errors
**Warning signs:** Error messages appear multiple times, errors from previous submission shown
### Pitfall 3: Different Field Structure in Additional Work Sections
**What goes wrong:** Assuming all fields work like furniture quantity inputs, breaking when fields are checkboxes or radio groups
**Why it happens:** Furniture sections use v{name}/q{name}/m{name} pattern, additional work uses diverse field types
**How to avoid:** Check field 'type' key in data structure, render appropriate HTML for each type
**Warning signs:** Form renders but fields don't submit data, email missing additional work sections
### Pitfall 4: Not Handling Empty Additional Work Sections in Email
**What goes wrong:** Email includes empty sections with just headers, bloating email unnecessarily
**Why it happens:** All sections rendered regardless of user input, unlike rooms which check has_items_with_quantities()
**How to avoid:** Implement has_additional_work_data() check before rendering each section
**Warning signs:** Email contains many empty "Montagearbeiten", "Schrank" headers
### Pitfall 5: Transient Key Collision in High Traffic
**What goes wrong:** Two users get same uniqid() in rare race condition, errors cross-contaminate
**Why it happens:** uniqid() uses microseconds but not guaranteed unique under extreme load
**How to avoid:** Use uniqid( '', true ) with more_entropy parameter for extra uniqueness
**Warning signs:** Extremely rare but catastrophic - user sees another user's validation errors
## Code Examples
Verified patterns from current codebase:
### Current Room Rendering Pattern (class-form-renderer.php lines 211-217)
```php
// Source: /includes/class-form-renderer.php
private static function render_all_rooms() {
$rooms = Umzugsliste_Furniture_Data::get_rooms();
foreach ( $rooms as $room_key => $room_label ) {
self::render_room_section( $room_key, $room_label );
}
}
```
### Current Email Room Generation Pattern (class-email-generator.php lines 135-156)
```php
// Source: /includes/class-email-generator.php
private static function generate_all_rooms( $data ) {
$html = '';
$rooms = Umzugsliste_Furniture_Data::get_rooms();
foreach ( $rooms as $room_key => $room_label ) {
$post_array_name = ucfirst( $room_key );
if ( 'kueche_esszimmer' === $room_key ) {
$post_array_name = 'Kueche_Esszimmer';
}
$room_data = $data[ $post_array_name ] ?? array();
// Only include room if it has items with quantities
if ( self::has_items_with_quantities( $room_data ) ) {
$html .= self::generate_room_section( $room_key, $room_label, $room_data );
}
}
return $html;
}
```
### Current Validation Pattern (class-form-handler.php lines 143-148)
```php
// Source: /includes/class-form-handler.php
foreach ( $required_fields as $field => $label ) {
if ( empty( $data[ $field ] ) ) {
$errors[] = 'Pflichtfeld fehlt: ' . $label;
}
}
```
### Current Sanitization Pattern (class-form-handler.php lines 204-213)
```php
// Source: /includes/class-form-handler.php
// Sanitize info array
if ( ! empty( $data['info'] ) && is_array( $data['info'] ) ) {
$sanitized['info'] = array();
foreach ( $data['info'] as $key => $value ) {
if ( 'eE-Mail' === $key ) {
$sanitized['info'][ $key ] = sanitize_email( $value );
} else {
$sanitized['info'][ $key ] = sanitize_text_field( $value );
}
}
}
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| session_id() for transient keys | Hidden field + uniqid() | 2020+ (caching era) | Sessions unreliable in cached environments |
| Separate form submission handlers | Single handler with validation pipeline | WordPress 5.0+ | Nonce + sanitization standardized |
| Manual HTML email tables | Template-based generation | Current (Phase 6) | Maintainable, matches legacy exactly |
**Deprecated/outdated:**
- PHP sessions for anonymous users: Unreliable in WordPress due to caching plugins and hosting restrictions
- $_SESSION variables: Not initialized by WordPress core, require manual session_start()
## Open Questions
None. All implementation details can be determined from existing code patterns.
## Sources
### Primary (HIGH confidence)
- Current plugin source code at /Users/vmiller/Local Sites/siegel/app/public/wp-content/plugins/Siegel-Umzugsliste/includes/
- class-form-handler.php - Error handling, validation, sanitization patterns
- class-form-renderer.php - Room section rendering, error display patterns
- class-furniture-data.php - Data structure for additional work sections (lines 230-295)
- class-email-generator.php - Email section generation patterns
- .planning/v1.0-MILESTONE-AUDIT.md - Gap specifications and requirements
### Secondary (MEDIUM confidence)
- [Creating unique transient name per browser session | WordPress.org](https://wordpress.org/support/topic/creating-unique-transient-name-per-browser-session/) - Hidden field pattern recommendation
- [PHP uniqid() Function | W3Schools](https://www.w3schools.com/php/func_misc_uniqid.asp) - uniqid() documentation with more_entropy parameter
- [wp_safe_redirect() Function | Developer.WordPress.org](https://developer.wordpress.org/reference/functions/wp_safe_redirect/) - Redirect with query parameters
- [add_query_arg() Function | Developer.WordPress.org](https://developer.wordpress.org/reference/functions/add_query_arg/) - Building redirect URLs with form_id
### Tertiary (LOW confidence)
- None required - all findings verified with primary sources
## Metadata
**Confidence breakdown:**
- Session bug fix: HIGH - WordPress community consensus, current code examination confirms issue
- Additional work sections: HIGH - Data structure exists, patterns established in current code
- Sonstiges field: HIGH - Simple textarea, standard WordPress sanitization
**Research date:** 2026-02-06
**Valid until:** 60 days (stable WordPress APIs, no fast-moving dependencies)