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() { + ?> +
" . nl2br( esc_html( $text ) ) . "
+