Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions wp-content/plugins/download-gateway/assets/js/gateway-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@
var personCookie = pendingPersonCookie;
var postId = pendingPostId;
var directUrl = pendingDirectUrl;
var isExternal = pendingIsExternal;

// Collect responses from intake form fields.
var responses = {};
Expand All @@ -641,7 +642,45 @@

intakeSubmitBtn.disabled = true;
intakeErrorMsg.textContent = '';
showLoadingStep();

// External downloads have no token to redeem — submit intake then redirect directly.
if ( isExternal || ! token ) {
doIntakeAndRedirect( postId, personCookie, responses, null, directUrl );
return;
}

// Step 1: Fetch the download URL via the JSON endpoint.
// This logs the redirect event server-side and returns {url, event_id}.
// Intake is submitted next so it can reference the correct download event.
fetch( gatewaySettings.downloadBase + '/' + token + '?format=json', {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': gatewaySettings.restNonce },
} )
.then( function ( res ) { return res.json().then( function ( d ) { return { ok: res.ok, data: d }; } ); } )
.then( function ( result ) {
var fileUrl = ( result.ok && result.data.url ) ? result.data.url : null;
var eventId = ( result.ok && result.data.event_id ) ? result.data.event_id : null;
// Fall back to direct token redirect if JSON response was not OK.
doIntakeAndRedirect( postId, personCookie, responses, eventId, fileUrl || ( gatewaySettings.downloadBase + '/' + token ) );
} )
.catch( function () {
// Network error — fall back to direct token redirect (server will 302).
doIntakeAndRedirect( postId, personCookie, responses, null, gatewaySettings.downloadBase + '/' + token );
} );
}

/**
* Submit the intake form then redirect to the file.
* Intake is supplementary: redirect always happens regardless of outcome.
*
* @param {number} postId WP post ID.
* @param {string} personCookie Signed gateway_gated cookie value.
* @param {Object} responses Key-value intake field responses.
* @param {number|null} downloadEventId ID of the just-logged redirect event, or null.
* @param {string} fileUrl File URL (resolved) or token URL (fallback).
*/
function doIntakeAndRedirect( postId, personCookie, responses, downloadEventId, fileUrl ) {
fetch( gatewaySettings.intakeUrl, {
method: 'POST',
credentials: 'same-origin',
Expand All @@ -650,21 +689,41 @@
'X-WP-Nonce': gatewaySettings.restNonce,
},
body: JSON.stringify( {
post_id: postId,
person_cookie: personCookie,
nonce: gatewaySettings.nonce,
responses: responses,
post_id: postId,
person_cookie: personCookie,
nonce: gatewaySettings.nonce,
responses: responses,
download_event_id: downloadEventId,
} ),
} )
.then( function () {
// Intake is supplementary — proceed regardless of server response.
finishDownload( token, directUrl );
triggerDownloadRedirect( fileUrl );
} )
.catch( function () {
finishDownload( token, directUrl );
// Intake is supplementary — proceed regardless.
triggerDownloadRedirect( fileUrl );
} );
}

/**
* Push the GA4 redirect event, close the modal, and navigate to the file.
* Used by the intake path after intake has been submitted.
*
* @param {string} fileUrl Resolved file URL or fallback token URL.
*/
function triggerDownloadRedirect( fileUrl ) {
pushEvent( {
event: 'resource_download_redirect',
post_id: pendingPostId || currentPostId,
post_type: pendingPostType || currentPostType,
policy: currentPolicy,
language_slug: currentLanguageSlug,
download_source: currentDownloadSource,
} );
closeModal();
redirect( fileUrl );
}

/**
* Close the modal and trigger the download. Called after intake submit.
*/
Expand Down
4 changes: 2 additions & 2 deletions wp-content/plugins/download-gateway/download-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Wikitongues Download Gateway
* Plugin URI: https://github.com/wikitongues/wikitongues.org
* Description: Signed download tokens, optional email gate, download event logging, and GA4 forwarding for all downloadable resources (document files, videos, captions, and future types).
* Version: 0.1.0
* Version: 0.1.13
* Requires at least: 6.0
* Requires PHP: 8.2
* Author: Wikitongues
Expand All @@ -17,7 +17,7 @@
exit;
}

define( 'GATEWAY_VERSION', '0.1.12' );
define( 'GATEWAY_VERSION', '0.1.13' );
define( 'GATEWAY_FILE', __FILE__ );
define( 'GATEWAY_DIR', plugin_dir_path( __FILE__ ) );
define( 'GATEWAY_REST_NAMESPACE', 'gateway/v1' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@

class DownloadController {

/**
* Stores the redirect event ID logged by the most recent get_file_url() call.
* Set as a side effect so handle() can include it in the ?format=json response
* without changing the testable resolve() return type.
*/
private ?int $last_event_id = null;

public function register_routes(): void {
register_rest_route(
GATEWAY_REST_NAMESPACE,
Expand Down Expand Up @@ -68,7 +75,13 @@ public function handle( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error
VisitorId::set_cookie( $visitor_id );

if ( 'json' === $format ) {
return new \WP_REST_Response( array( 'url' => $result ), 200 );
return new \WP_REST_Response(
array(
'url' => $result,
'event_id' => $this->last_event_id,
),
200
);
}

header( 'Cache-Control: no-store, no-cache, must-revalidate' );
Expand Down Expand Up @@ -182,8 +195,8 @@ private function get_file_url(
return new \WP_Error( 'file_not_found', 'File could not be resolved.', array( 'status' => 404 ) );
}

$post_type = (string) get_post_type( $post_id );
$event_id = DownloadEventRepository::log(
$post_type = (string) get_post_type( $post_id );
$event_id = DownloadEventRepository::log(
array(
'post_id' => $post_id,
'post_type' => $post_type,
Expand All @@ -194,6 +207,7 @@ private function get_file_url(
'ip_hash' => IpHasher::hash_from_server( $server ),
)
);
$this->last_event_id = is_int( $event_id ) ? $event_id : null;

// Enqueue download webhook if an endpoint is configured.
$endpoint = SettingsRepository::get_webhook_endpoint();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ public function handle( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error
$responses = array();
}

$raw_event_id = $request->get_param( 'download_event_id' );
$download_event_id = is_numeric( $raw_event_id ) ? (int) $raw_event_id : null;

$result = $this->submit(
(int) ( $request->get_param( 'post_id' ) ?? 0 ),
(string) ( $request->get_param( 'person_cookie' ) ?? '' ),
(string) ( $request->get_param( 'nonce' ) ?? '' ),
$responses
$responses,
$download_event_id
);

if ( is_wp_error( $result ) ) {
Expand All @@ -61,13 +65,15 @@ public function handle( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error
/**
* Process an intake submission. Returns true on success, WP_Error on failure.
*
* @param int $post_id Post ID of the downloaded resource.
* @param string $person_cookie HMAC-signed cookie value from gateway_gated.
* @param string $nonce WP nonce value.
* @param array<string,string> $responses Key-value map of field responses.
* @param int $post_id Post ID of the downloaded resource.
* @param string $person_cookie HMAC-signed cookie value from gateway_gated.
* @param string $nonce WP nonce value.
* @param array<string,string> $responses Key-value map of field responses.
* @param int|null $download_event_id ID of the redirect event from wp_gateway_download_events.
* Passed by the JS after fetching the download URL.
* @return true|\WP_Error
*/
public function submit( int $post_id, string $person_cookie, string $nonce, array $responses ): bool|\WP_Error {
public function submit( int $post_id, string $person_cookie, string $nonce, array $responses, ?int $download_event_id = null ): bool|\WP_Error {
if ( ! wp_verify_nonce( $nonce, 'gateway_gate' ) ) {
return new \WP_Error( 'invalid_nonce', 'Request could not be verified.', array( 'status' => 403 ) );
}
Expand Down Expand Up @@ -103,7 +109,7 @@ public function submit( int $post_id, string $person_cookie, string $nonce, arra
array(
'type' => 'intake',
'person_id' => $person_id,
'download_event_id' => DownloadEventRepository::find_redirect_id( $person_id, $post_id ),
'download_event_id' => $download_event_id,
'post_id' => $post_id,
'post_type' => $post_type,
'airtable_record_id' => get_post_meta( $post_id, '_airtable_record_id', true ) ?: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static function dispatch_pending(): void {
"SELECT * FROM {$wpdb->prefix}gateway_webhook_delivery
WHERE status IN ('pending','failed')
AND next_attempt_at <= %s
ORDER BY id ASC
LIMIT 50",
current_time( 'mysql' )
)
Expand Down
Loading