Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Phoenix Nest — Booking → E‑Signature Bridge

WordPress PHP License Status Tests

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.


Why this exists

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:

  1. 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.
  2. Prefill transport — WP E‑Signature's [esigget] merge fields read the original request via filter_input(), so values must ride the signing URL as real query parameters.
  3. 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.

Architecture

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/
Loading

The three hooks

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.

v1.3.0 — the agreement "disappeared" from the second admin's document list (2026‑08‑05)

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_id with no filter, and User::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:

  1. The row set, through the plugin's own esig-document-index-docs filter, by re‑running fetchAllOnStatus( $status, true ) — its unscoped super‑admin branch.
  2. The tab counts, by priming the object‑cache key getDocumentsTotal() checks first. Only a non‑zero count is ever written: if a tab keys on document_type rather than document_status, our count is 0 and 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.


v1.2.2 — the redirect never actually fired (2026‑08‑04)

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.

  1. booking_id is never sent. MotoPress Booking Calendar 1.3.3 puts only payment_id + token on 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.
  2. 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 without mpbc_action. Matching only the first shape means configuring the page silently re‑breaks the redirect. Now keyed on mpbc_action or (payment_id + token) — widening the match is safe because the short‑lived pending transient, not the query string, is the real gate.
  3. 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 as mpbc_customer_* elsewhere in this plugin). Reading only the unprefixed key returns '', so $booking_id stayed 0 and the redirect bailed with no error anywhere. Both keys are now read, then wp_get_post_parent_id() as a last resort.

Two host‑side notes that are not code but will waste your afternoon:

  • Reservation Received Page must contain the Booking Calendar block. MotoPress registers no separate confirmation block — mpbc/booking-calendar is 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.


v1.1.0 — linking a booking to its signed document

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_sig ride 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 is sha1( $doc_id . $bakedContent ) and changes if the document is ever re-saved, so it is resolved at render time via document_checksum_by_id().

Two decisions worth knowing before touching it:

  1. The guard is an HMAC this plugin mints itself. A pn_bid arriving through HTTP_REFERER is attacker-controlled, so without a signature a forged referer could stamp _pn_esign_doc_id onto any post on the site. wp_salt() means one cannot be forged.
  2. First clone wins. copyDocument() runs once per signer, so a counter-signature or a re-send would otherwise overwrite the renter's document id.

Installation

# From wp-content/plugins/
git clone https://github.com/codebyshoaib/phoenix-esign-bridge.git

Or 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+.


Configuration

The plugin is convention‑driven and needs no settings page. To wire it to your document:

  1. 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]).
  2. Set the document's post‑sign redirect to your confirmation page (e.g. /booking-confirmed/).
  3. Point the signing page path via the pn_esign_page_path filter 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().


Engineering notes

  • Bake at clone time, never after signing. Rewriting content after esig_signature_saved would 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."

Tests

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.


Part of a four-plugin system

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
Loading

_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.


Compatibility

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.


Author

Shoaib Ud Din — Full‑stack engineer LinkedIn · GitHub

License

GPL‑2.0‑or‑later — consistent with the WordPress plugin ecosystem.

About

WordPress plugin bridging MotoPress bookings to pre-filled, e-signed rental agreements (WP E-Signature): auto-prefill, confirmation redirect, and tamper-safe value baking. Live on phoenixnest.properties.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages