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:
379
.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md
Normal file
379
.planning/phases/08-bugfixes-legacy-parity/08-RESEARCH.md
Normal 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)
|
||||
Reference in New Issue
Block a user