Private media storage, signed time-limited download links, and why email attachments are the wrong document delivery tool.
The document exchange problem is simple to describe and annoying to solve well. Customers need to receive and submit documents — title paperwork, financing agreements, inspection reports, purchase agreements. Before the portal, this happened over email attachments: no version control, no access logging, no single source of truth, and documents going to personal email addresses where they lived forever and couldn’t be revoked.
A typical transaction at the Anchor dealership involved 4–6 documents: purchase agreement, title, bill of sale, financing paperwork, inspection report, consignment agreement. These were emailed as PDF attachments, returned signed via reply, saved to a shared drive folder with inconsistent naming, and occasionally lost. When a customer called asking for a copy of their purchase agreement from 18 months ago, finding it required searching multiple places.
Documents are stored in WordPress media in a private directory not publicly accessible. Each document is associated with a specific owned_unit record and tagged with the document type. MIME type is validated from the file’s actual bytes, not the extension:
function [client]_attach_document_to_transaction(
string $file_path,
string $original_filename,
int $lead_id,
int $unit_id,
string $doc_type
): int|false {
// Validate file type from bytes — not extension
$allowed_types = [ 'application/pdf', 'image/jpeg', 'image/png' ];
$mime_type = mime_content_type( $file_path );
if ( ! in_array( $mime_type, $allowed_types, true ) ) {
return false;
}
// Validate file size — 20MB max
if ( filesize( $file_path ) > 20 * MB_IN_BYTES ) {
return false;
}
$attachment_id = media_handle_sideload(
[
'name' => sanitize_file_name( $original_filename ),
'tmp_name' => $file_path,
],
0
);
if ( is_wp_error( $attachment_id ) ) return false;
// Mark as private — not accessible via direct URL
update_post_meta( $attachment_id, '[client]_doc_private', '1' );
update_post_meta( $attachment_id, '[client]_doc_type', sanitize_key( $doc_type ) );
update_post_meta( $attachment_id, '[client]_doc_lead_id', $lead_id );
update_post_meta( $attachment_id, '[client]_doc_unit_id', $unit_id );
[client]_add_conversation(
$lead_id,
'system',
"Document attached: {$doc_type} — {$original_filename}",
'Document upload'
);
return $attachment_id;
}
Documents are not served from their direct media URL. The portal generates a signed HMAC token valid for a limited time window, verifiable server-side without a database lookup:
function [client]_generate_document_token( int $attachment_id, int $ttl_seconds = 3600 ): string {
$expires = time() + $ttl_seconds;
$secret = wp_salt( 'auth' );
$sig = hash_hmac( 'sha256', $attachment_id . ':' . $expires, $secret );
return base64_encode( json_encode( [ 'id' => $attachment_id, 'exp' => $expires, 'sig' => $sig ] ) );
}
function [client]_verify_and_serve_document( string $token ): void {
$data = json_decode( base64_decode( $token ), true );
$attachment_id = (int) ( $data['id'] ?? 0 );
$expires = (int) ( $data['exp'] ?? 0 );
$sig = $data['sig'] ?? '';
if ( ! $attachment_id || ! $expires || ! $sig ) {
wp_die( 'Invalid token.', 400 );
}
if ( time() > $expires ) {
wp_die( 'Link expired. Please request a new download link from the portal.', 410 );
}
$secret = wp_salt( 'auth' );
$expected = hash_hmac( 'sha256', $attachment_id . ':' . $expires, $secret );
// hash_equals() is constant-time — === is vulnerable to timing attacks
if ( ! hash_equals( $expected, $sig ) ) {
wp_die( 'Invalid token.', 403 );
}
// Verify portal user owns this document
$doc_lead_id = (int) get_post_meta( $attachment_id, '[client]_doc_lead_id', true );
$user_lead = (int) get_user_meta( get_current_user_id(), '[client]_portal_lead_id', true );
if ( $doc_lead_id !== $user_lead ) {
wp_die( 'Access denied.', 403 );
}
// Serve the file
$file_path = get_attached_file( $attachment_id );
$mime_type = get_post_mime_type( $attachment_id );
header( 'Content-Type: ' . $mime_type );
header( 'Content-Disposition: attachment; filename="' . basename( $file_path ) . '"' );
header( 'Content-Length: ' . filesize( $file_path ) );
header( 'Cache-Control: private, no-cache' );
readfile( $file_path );
exit;
}
Customers can upload documents from the portal — signed paperwork, additional photos, any document staff requested. The upload handler validates file type, size, and confirms the user is uploading to a transaction that belongs to them:
add_action( 'wp_ajax_[client]_portal_upload', '[client]_handle_portal_document_upload' );
function [client]_handle_portal_document_upload() {
if ( ! wp_verify_nonce( $_POST['_wpnonce'], '[client]_portal_upload' ) ) {
wp_send_json_error( 'Security check failed.' );
return;
}
$unit_id = (int) ( $_POST['unit_id'] ?? 0 );
$doc_type = sanitize_key( $_POST['doc_type'] ?? 'customer_upload' );
// Verify ownership
$lead_id = (int) get_user_meta( get_current_user_id(), '[client]_portal_lead_id', true );
if ( ! $lead_id || ! $unit_id ) {
wp_send_json_error( 'Invalid request.' );
return;
}
if ( ! isset( $_FILES['document'] ) || $_FILES['document']['error'] !== UPLOAD_ERR_OK ) {
wp_send_json_error( 'Upload failed.' );
return;
}
$attachment_id = [client]_attach_document_to_transaction(
$_FILES['document']['tmp_name'],
$_FILES['document']['name'],
$lead_id,
$unit_id,
$doc_type
);
if ( ! $attachment_id ) {
wp_send_json_error( 'Invalid file type or size.' );
return;
}
// Notify staff of customer upload
wp_mail(
SALES_EMAIL,
'[Portal] Customer document uploaded: ' . get_the_title( $unit_id ),
"Lead #{$lead_id} uploaded a document ({$doc_type}) for unit " . get_the_title( $unit_id ),
[ 'Content-Type: text/html; charset=UTF-8' ]
);
wp_send_json_success( [ 'attachment_id' => $attachment_id ] );
}
Signed time-limited URLs solve the revocation problem. If a document is sent via email, it exists in the recipient’s inbox indefinitely and cannot be revoked. A portal download link that expires in 1 hour can be regenerated when needed and is invalid outside that window.
The ownership check in verify_and_serve_document() is critical. Without it, anyone who obtains a valid token (forwarded link, shared URL) can download any document — a customer’s purchase agreement, another consignor’s contract. The lead ID comparison ensures the serving user is authorized for that specific document.
The document exchange replaced the email attachment workflow for all new transactions starting in month one after portal launch. Average document turnaround dropped from 3.2 days (email attachment, sign, scan, email back) to same-day for customers who used the portal. Staff spent about 4 fewer hours per week on document-related email back-and-forth.
The one-hour token TTL was reduced from 24 hours after a staff member forwarded a download link to a personal email to check something on their phone. That link was technically valid for 24 hours in an uncontrolled context. One hour is the right default — customers can regenerate instantly if they need the file again.
mime_content_type(), not file extension. Extensions are trivially faked. MIME type from the file’s actual bytes is the correct validation.hash_equals() for signature comparison, not ===. hash_equals() is constant-time. === is vulnerable to timing attacks on the signature comparison.Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →