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 post‑booking return, gathers all booking meta into the pn_* prefill map, and redirects the renter to the signing page. Matches both MotoPress return‑URL shapes and resolves the booking from payment_id (v1.2.2 — see below). 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. |
esig-document-index-docs + esig_documents_loading |
pn_esign_widen_document_list / _counts |
v1.3.0 — lets a second WordPress administrator see the documents in E‑Signature → My Documents, which the plugin otherwise scopes to a single stored super admin. Read‑only widening; changes no ownership and no notification recipient. |
Once the rental agreement was owned by the client's own account, E‑Signature → My Documents went
empty for the developer/second administrator account. Nothing was deleted: WP E‑Signature scopes that
screen to one stored super admin (option esig_superadmin_user) and shows every other administrator
only rows whose esign_documents.user_id equals their own id.
The two obvious fixes are both wrong here:
- Move the super‑admin slot.
is_esig_super_admin()is a bare$wp_user_id == $admin_user_idwith no filter, andUser::checkEsigAdmin()gates plugin access on it — so moving the slot locks the other admin out of the plugin entirely. - Reassign document ownership. The auto‑added owner counter‑signature and the completion
notification both follow
esign_documents.user_id. Reassigning ownership re‑points the counter‑signature at the wrong person and mails the wrong administrator — that is the bug this release is careful not to reintroduce.
So the bridge widens only the two things the screen actually reads, for a user who already holds
manage_options, and nothing else:
- The row set, through the plugin's own
esig-document-index-docsfilter, by re‑runningfetchAllOnStatus( $status, true )— its unscoped super‑admin branch. - The tab counts, by priming the object‑cache key
getDocumentsTotal()checks first. Only a non‑zero count is ever written: if a tab keys ondocument_typerather thandocument_status, our count is0and writing it would hide a number the plugin computes correctly.
A search result is left alone — fetchAllonSearch() is already unscoped. This grants no capability a
WordPress administrator does not already have, and touches neither esig_superadmin_user, document
ownership, nor any signature row.
Walking the live order flow end to end showed the renter was never reaching the agreement: every booking landed on the confirmation page and stopped. Three independent faults, each on its own enough to break the redirect — so fixing any two still looks broken. Verify all three before concluding.
booking_idis never sent. MotoPress Booking Calendar 1.3.3 puts onlypayment_id+tokenon the post‑booking URL. The old code read$_GET['booking_id']and bailed immediately, every time. The booking is now resolved from the payment post.- There are two different return‑URL shapes. With no Reservation Received Page configured,
MotoPress redirects to
home_url()with?mpbc_action=booking-confirmation; once that page is configured it redirects to the page withoutmpbc_action. Matching only the first shape means configuring the page silently re‑breaks the redirect. Now keyed onmpbc_actionor (payment_id+token) — widening the match is safe because the short‑lived pending transient, not the query string, is the real gate. - The meta key is not the metabox field name. The payment metabox field is
mpbc_booking_id, but the stored meta key is the underscore‑protected_mpbc_booking_id(the same split asmpbc_customer_*elsewhere in this plugin). Reading only the unprefixed key returns'', so$booking_idstayed0and the redirect bailed with no error anywhere. Both keys are now read, thenwp_get_post_parent_id()as a last resort.
Two host‑side notes that are not code but will waste your afternoon:
Reservation Received Pagemust contain the Booking Calendar block. MotoPress registers no separate confirmation block —mpbc/booking-calendaris what renders the confirmation view — so pointing the setting at an ordinary page yields a blank page, and leaving it unset sends the renter to the site homepage.- The confirmation view is single‑use. Its token is consumed on first render; re‑loading the same URL shows a bare calendar. That is not a regression.
Verified live: booking → /motor-vehicle-rental-agreement/ with every pn_* value pre‑filled and
rendered in the document body.
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.