diff --git a/.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md b/.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md new file mode 100644 index 0000000..52954f8 --- /dev/null +++ b/.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md @@ -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 ''; + +// 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() { + ?> +
+
+
+

Sonstiges

+
+
+ + +
+
+
+ +
+

Sonstiges

+

" . nl2br( esc_html( $text ) ) . "

+
+ "; +} +``` + +## 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)