Developer documentation for the CSV subsystem of the WPResidence real estate CRM. Entry point: libs/csv-functions.php.
Supported Entities
Contacts, leads, enquiries, deals, tasks. Export is gated to contacts, deals, tasks in the hardcoded whitelist; import supports all five.
Export
| Property | Value |
|---|---|
| AJAX action | wpestate_crm_export_csv |
| Nonce | wpestate_crm_nonce (security param) |
| Capability | wpestate_crm_user_can(‘import_export’, ‘view’) |
| Function | wpestate_crm_export_csv($entity_type, $args) |
CSV Output
- Prepends UTF-8 BOM (\xEF\xBB\xBF) so Excel opens correctly on non-Latin columns.
- Every value wrapped in double quotes; internal double quotes escaped by doubling.
- HTTP headers: Content-Type: text/csv; charset=utf-8, Content-Disposition: attachment; filename=crm_{entity}_{YYYY-MM-DD}.csv.
- Terminates with die() to prevent WordPress from appending its own output.
- Record limit: 10000 per export.
Ownership Scoping
Non-admins have their user_id injected into the query args, so they only export their own records.
Import — Upload Phase
| Property | Value |
|---|---|
| AJAX action | wpestate_crm_upload_csv |
| Nonce | wpestate_crm_nonce |
| Capability | wpestate_crm_user_can(‘import_export’, ‘create’) |
Flow:
- Validate $_FILES[‘csv_file’].
- Ensure /wp-content/uploads/wpestate-crm/ exists.
- wp_unique_filename() to produce a safe filename.
- Move the upload into the CRM directory.
- Parse header row, stripping BOM if present.
- Read first 5 data rows for preview.
- Return JSON: file_path (basename only), headers, preview_rows, total_rows.
Column Mapping
wpestate_crm_map_csv_columns( $csv_headers, $entity_type )
Normalization: lowercase, trim, replace spaces with underscores. Direct match against valid columns first, then alias map. Common aliases: name → first_name, e-mail → email, tel → phone, cell → mobile, zip → zipcode, title → deal_title, value → deal_value, stage → deal_stage, priority → task_priority, type → task_type.
Import — Batch Phase
| Property | Value |
|---|---|
| AJAX action | wpestate_crm_import_csv_batch |
| Nonce | wpestate_crm_nonce |
| Capability | wpestate_crm_user_can(‘import_export’, ‘create’) |
| Params | file_path (basename), offset, batch_size (default 100), duplicate_action (skip or update) |
Path-Traversal Safeguards
The batch handler accepts only a bare filename and reconstructs the full path server-side:
- sanitize_file_name() + basename().
- Reject filenames with path separators or colons.
- Construct path from known base /wp-content/uploads/wpestate-crm/.
- realpath() validates the resolved path stays inside the allowed directory (defends against symlink escapes).
- MIME / extension check: only .csv or .txt accepted.
- is_file() rejects directories and device nodes.
Per-Row Processing
- Read row.
- For contacts: wpestate_crm_find_contact_by_email() to detect duplicates.
- For leads/enquiries: wpestate_crm_detect_duplicate($entity_type, $email).
- If duplicate + skip: increment skipped counter.
- If duplicate + update: call wpestate_crm_update_contact() (merge mode).
- If no duplicate: call wpestate_crm_insert_contact().
Response
{ imported: int, updated: int, skipped: int, errors: [{row, message}, ...], done: bool, next_offset: int }
Duplicate Detection
wpestate_crm_detect_duplicate( $entity_type, $email )
Queries the relevant table by email column: contacts use email; leads use from_email; enquiries use from_email. Returns the matching ID or false.
Hooks Fire Per Row
Each inserted contact fires wpestate_crm_after_insert_contact. That cascades into email notifications, HubSpot sync, webhooks, automations. For large imports, either pause automations first or suspend the hooks temporarily in your import wrapper.
Extending
- Add new entity type support by extending the validator whitelist and mapping the entity’s CRUD functions into the batch processor.
- Replace the alias map with a filter hook if you want to expose it to customers.
- For very large imports (>100K rows), write a WP-CLI command that reads the CSV and calls the CRUD functions directly, bypassing the AJAX batching.