A single‑file WordPress plugin that turns a MotoPress vehicle booking into a pre‑filled, legally e‑signed Motor Vehicle Rental Agreement via WP E‑Signature — with the renter's data captured once and baked into the signed PDF so the executed copy is tamper‑correct, not a blank template.
Live in production: phoenixnest.properties — Phoenix Nest Properties LLC (Destin, FL vehicle rentals). A customer books a vehicle, is redirected into a pre‑filled agreement, signs once, and receives a completed PDF; the owner counter‑signature is applied automatically.
Off‑the‑shelf, MotoPress and WP E‑Signature don't talk to each other. A rental business needs the booking a customer just made to flow straight into a specific, pre‑filled, legally‑binding agreement — no re‑typing, no blank fields, no manual document creation per booking. This plugin is the integration layer that makes that a single, hands‑off flow.
It solves three problems that a naïve "just add a shortcode" approach gets wrong:
- Timing — MotoPress writes customer data after the initial save, and submits over
admin-ajax, so you can't read the data or steer the browser at submit time. - Prefill transport — WP E‑Signature's
[esigget]merge fields read the original request viafilter_input(), so values must ride the signing URL as real query parameters. - Persistence — merge‑field values render on screen but do not persist into the signed copy by default; the multi‑step signing flow re‑renders the document without them, leaving blanks in the executed PDF.
sequenceDiagram
actor R as Renter
participant MP as MotoPress Booking
participant BR as Bridge Plugin
participant ES as WP E-Signature
R->>MP: Submit booking (dates, address, DL, insurance)
MP->>BR: save_post (pri 20) — flag booking "pending e-sign" (15-min transient)
MP-->>R: Redirect to booking-confirmation view
R->>BR: template_redirect intercepts the confirmation
Note over BR: Gather booking meta →<br/>build the pn_* prefill map
BR-->>R: 302 → signing page with ?pn_*=… prefill
R->>ES: Review pre-filled agreement, draw/type signature
ES->>BR: esig_document_clone_render_content (per-signer clone created)
Note over BR: Bake pn_* values into the clone<br/>as static text — BEFORE signature capture
ES->>ES: Capture signature on the baked clone (tamper-integrity holds)
ES-->>R: Store signed PDF + auto owner counter-sign → /booking-confirmed/
| Hook | Function | Responsibility |
|---|---|---|
save_post (pri 20) |
pn_esign_mark_pending |
Flags a front‑end booking as pending e‑sign. Correctly distinguishes a real booking (admin-ajax) from a genuine wp‑admin edit. |
template_redirect |
pn_esign_confirmation_redirect |
Intercepts MotoPress's confirmation view, gathers all booking meta into the pn_* prefill map, and redirects the renter to the signing page. The pending‑flag gate prevents booking‑ID enumeration from leaking renter PII. |
esig_document_clone_render_content |
pn_esign_store_doc_id |
v1.1.0 — records which signed document belongs to which booking (_pn_esign_doc_id), the link nothing stored before. Only writes when the request carries a matching HMAC over the booking id, because that id can arrive via an attacker‑controlled referer. |
esig_document_clone_render_content |
pn_esign_bake_clone |
The persistence fix: when WP E‑Signature clones the master document for a signer, replaces every [esigget pn_*] merge field with the real value as static text, before the signature is captured — so the signed content is exactly what was signed. |
Nothing recorded which signed document belonged to which booking, so anything downstream could only guess an agreement from name + date. v1.1.0 closes that:
pn_bid+pn_sigride the signing URL alongside the existing prefill, so they survive the same referer fallback.- At clone time,
pn_esign_store_doc_id()validates and writes_pn_esign_doc_id. - Only the document id is stored, never the checksum. The
did=hash in the PDF URL issha1( $doc_id . $bakedContent )and changes if the document is ever re-saved, so it is resolved at render time viadocument_checksum_by_id().
Two decisions worth knowing before touching it:
- The guard is an HMAC this plugin mints itself. A
pn_bidarriving throughHTTP_REFERERis attacker-controlled, so without a signature a forged referer could stamp_pn_esign_doc_idonto any post on the site.wp_salt()means one cannot be forged. - First clone wins.
copyDocument()runs once per signer, so a counter-signature or a re-send would otherwise overwrite the renter's document id.
# From wp-content/plugins/
git clone https://github.com/codebyshoaib/phoenix-esign-bridge.gitOr download the ZIP and upload via Plugins → Add New → Upload Plugin, then Activate.
Requirements: WordPress with MotoPress Booking Calendar, WP E‑Signature (core + Signer Input Fields / Stand Alone Documents add‑ons), PHP 7.4+.
The plugin is convention‑driven and needs no settings page. To wire it to your document:
- In WP E‑Signature, build a Stand Alone document and drop
[esigget pn_key]merge fields into the body (e.g.[esigget pn_renter_name],[esigget pn_total_due]). - Set the document's post‑sign redirect to your confirmation page (e.g.
/booking-confirmed/). - Point the signing page path via the
pn_esign_page_pathfilter if it differs from the default:
add_filter( 'pn_esign_page_path', fn() => '/motor-vehicle-rental-agreement/' );Any pn_* merge key you add to the document is covered automatically by the bake step. The prefill map (name, address, driver's licence, pickup/return, total due, additional drivers, …) is assembled in pn_esign_build_prefill().
- Bake at clone time, never after signing. Rewriting content after
esig_signature_savedwould break WP E‑Signature's tamper checksum. The clone filter fires on the raw decrypted content before checksum + encrypt + store — the only correct place to inject values. - PII stays off persistent storage. Prefill rides a single, one‑time confirmation→signing redirect gated by a short‑lived transient; there's no booking‑ID endpoint that echoes renter data.
- Packaged as its own plugin, not appended to another. Isolation means a host or theme update can't silently wipe the integration, and a fatal in this code disables only the feature — it never white‑screens the site.
- Watch the PHP function‑hoisting trap. A top‑level
if ( function_exists('…') ) return;"safety guard" is a footgun here: PHP hoists top‑level function definitions at compile time, so such a guard is always true and the plugin returns before registering any hooks — functions exist, but nothing is wired. Verify integrations by confirming the hook is actually attached (or with an end‑to‑end signed‑PDF check), not merely "no fatal error."
php tests/selftest.php # → OK (31 assertions)No framework, no WordPress: it stubs the WP functions the plugin touches and asserts the two things that fail silently — that the hooks are actually attached (see the hoisting note above, which cost this plugin a production bug), and that the doc-id guard rejects an unsigned id, a wrong signature, a correctly-signed id that is not a booking, and a post that does not exist. Nothing local replaces the end-to-end signing check.
Each plugin owns one job and one set of meta keys, so a bug in a reporting screen can never take down the booking flow.
| Plugin | Job | |
|---|---|---|
| 1 | Custom booking fields (site-specific, not published) | Injects and saves renter fields on the booking form → _phoenix_* |
| 2 | phoenix-esign-bridge (this repo) | Booking → pre-filled agreement → signed PDF, and records which document belongs to which booking |
| 3 | phoenix-vehicle-condition | Pickup + return condition photos at handover → _pn_vc_* |
| 4 | phoenix-customer-records | Read-only: groups every booking into customers and shows all of the above in one place |
flowchart LR
F["1 · booking fields<br/><code>_phoenix_*</code>"] --> E["2 · e-sign bridge<br/><code>_pn_esign_doc_id</code>"]
E --> V["3 · condition photos<br/><code>_pn_vc_*</code> + attachments"]
F --> R["4 · customer records<br/>reads only"]
E --> R
V --> R
_pn_esign_doc_id is the seam between 2 and 4: plugin 4's agreement link only works for documents
signed after v1.1.0 went live, because a link that was never recorded cannot be backfilled.
Validated against WP E‑Signature 2.1.x and MotoPress Booking Calendar. The bake step depends on the esig_document_clone_render_content filter; after any major WP E‑Signature update, run one test signing and confirm the resulting PDF contains real field values.
Shoaib Ud Din — Full‑stack engineer LinkedIn · GitHub
GPL‑2.0‑or‑later — consistent with the WordPress plugin ecosystem.