get_site();
$evidence = Availability::has( Availability::EVIDENCE );
$recent = $evidence ? $client->list_evidence(
array(
'limit' => (string) self::RECENT_LIMIT,
'include_test' => 'true',
)
) : array( 'body' => array( 'data' => array() ) );
?>
';
return;
}
$first = isset( $site['body']['first_evidence_at'] ) && is_string( $site['body']['first_evidence_at'] ) ? strtotime( $site['body']['first_evidence_at'] ) : false;
self::render_first_evidence( $first );
if ( $evidence ) {
self::render_recent( isset( $recent['body']['data'] ) && is_array( $recent['body']['data'] ) ? $recent['body']['data'] : array() );
self::render_export();
} else {
echo '
' . esc_html__( 'Your consent records, and exports for an audit, are in your Consiva dashboard.', 'consiva-ai' ) . ' ' . esc_html__( 'Open the Consiva dashboard', 'consiva-ai' ) . ' ' . esc_html__( '(opens in a new tab)', 'consiva-ai' ) . '
';
}
?>
self::FRESH_DAYS * DAY_IN_SECONDS ) {
/* translators: %s: date and time in IST. */
echo '' . esc_html( sprintf( __( 'Recording evidence since your first visitor decision on %s.', 'consiva-ai' ), $when ) ) . '
';
return;
}
?>
$items Evidence summaries, newest first.
* @return void
*/
private static function render_recent( array $items ) {
if ( ! $items ) {
return;
}
?>
$job Stored job.
* @return void
*/
private static function render_job( array $job ) {
$status = ( new ConsivaClient() )->get_evidence_export( (string) $job['job_id'] );
if ( is_wp_error( $status ) ) {
Screens::render_error( $status );
return;
}
$body = $status['body'];
$state = isset( $body['status'] ) ? sanitize_key( (string) $body['status'] ) : '';
$count = isset( $body['record_count'] ) ? (int) $body['record_count'] : null;
echo '';
if ( ! empty( $job['window_clamped'] ) && ! empty( $job['from'] ) ) {
/* translators: %s: date. */
echo '
' . esc_html( sprintf( __( 'Your plan covers records from %s onwards. Earlier records are kept at Consiva; a higher plan lets you export them.', 'consiva-ai' ), wp_date( get_option( 'date_format' ), (int) strtotime( (string) $job['from'] ) ) ) ) . '
';
}
if ( 'completed' === $state ) {
?>
' . esc_html__( 'Consiva could not prepare this export. Try again; if it keeps failing, contact support.', 'consiva-ai' ) . '';
} else {
echo '
' . esc_html__( 'Preparing your export. This usually takes a few seconds.', 'consiva-ai' ) . ' ' . esc_html__( 'Check again', 'consiva-ai' ) . '
';
}
echo '
';
}
// ------------------------------------------------------------------ Handlers
/**
* "Send a test decision" (Runtime §5.2): stamped test = true, so it never counts as first evidence.
*
* @return void
*/
public function handle_test_decision() {
self::guard( self::ACTION_TEST );
$map = CategoryMap::get();
if ( null === $map ) {
self::finish( new WP_Error( 'consiva_consent_no_token', __( 'Consent controls are not fully set up on this site yet. Activate them first, then send a test decision.', 'consiva-ai' ) ) );
}
$result = ( new ConsivaClient() )->send_consent_events(
$map['token'],
array(
array(
'event_id' => 'wp_test_' . bin2hex( random_bytes( 8 ) ),
'action' => 'RejectAll',
'categories' => array(
'necessary' => true,
'preferences' => false,
'statistics' => false,
'marketing' => false,
),
'url' => home_url( '/' ),
'test' => true,
),
)
);
if ( is_wp_error( $result ) ) {
self::finish( $result );
}
$item = isset( $result['body']['results'][0] ) && is_array( $result['body']['results'][0] ) ? $result['body']['results'][0] : array();
if ( ! isset( $item['status'] ) || 'validated' !== $item['status'] ) {
$why = isset( $item['error'] ) && is_string( $item['error'] ) ? sanitize_text_field( $item['error'] ) : __( 'no reason given', 'consiva-ai' );
/* translators: %s: Consiva's reason. */
self::finish( new WP_Error( 'consiva_consent_test_rejected', sprintf( __( 'Consiva did not accept the test decision: %s', 'consiva-ai' ), $why ), array( 'request_id' => $result['request_id'] ) ) );
}
self::finish( null, __( 'Test decision accepted. Consiva checked it and, because it is a test, did not store it.', 'consiva-ai' ), $result['request_id'] );
}
/**
* Starts an export job (Runtime §5.3). Consiva applies the plan's export window.
*
* @return void
*/
public function handle_export() {
self::guard( self::ACTION_EXPORT );
$format = isset( $_POST['format'] ) && 'csv' === $_POST['format'] ? 'csv' : 'json'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard().
// No "from": Consiva exports everything the plan's window allows and says if it clamped.
$result = ( new ConsivaClient() )->start_evidence_export( array( 'format' => $format ) );
if ( is_wp_error( $result ) ) {
self::finish( $result );
}
$body = $result['body'];
$id = isset( $body['job_id'] ) && is_string( $body['job_id'] ) ? $body['job_id'] : '';
if ( ! preg_match( ScanProgress::ID_PATTERN, $id ) ) {
self::finish( new WP_Error( 'consiva_consent_invalid_payload', __( 'Consiva started the export but did not say which one. Try again.', 'consiva-ai' ) ) );
}
State::update(
array(
self::JOB_STATE_KEY => array(
'job_id' => $id,
'format' => $format,
'window_clamped' => ! empty( $body['window_clamped'] ),
'from' => isset( $body['from'] ) ? sanitize_text_field( (string) $body['from'] ) : '',
),
)
);
self::finish( null, __( 'Preparing your export.', 'consiva-ai' ), $result['request_id'] );
}
/**
* Streams the finished export to the admin, unchanged.
*
* @return void
*/
public function handle_download() {
self::guard( self::ACTION_DOWNLOAD );
$job = State::get( self::JOB_STATE_KEY );
if ( ! is_array( $job ) || empty( $job['job_id'] ) ) {
self::finish( new WP_Error( 'consiva_consent_no_export', __( 'There is no export to download. Prepare one first.', 'consiva-ai' ) ) );
}
$file = ( new ConsivaClient() )->download_evidence_export( (string) $job['job_id'] );
if ( is_wp_error( $file ) ) {
self::finish( $file );
}
$is_csv = 'csv' === $job['format'];
nocache_headers();
header( 'Content-Type: ' . ( $is_csv ? 'text/csv' : 'application/json' ) . '; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="consiva-evidence-' . sanitize_file_name( (string) $job['job_id'] ) . ( $is_csv ? '.csv' : '.json' ) . '"' );
header( 'X-Content-Type-Options: nosniff' );
header( 'Content-Length: ' . strlen( $file['raw'] ) );
echo $file['raw']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- a file download, byte for byte, so it still verifies.
exit;
}
// ------------------------------------------------------------------ Helpers
/**
* Date and time in IST (Runtime §5.2 wording).
*
* @param int $timestamp Unix time.
* @return string
*/
private static function ist( $timestamp ) {
/* translators: %s: formatted date and time. */
return sprintf( __( '%s IST', 'consiva-ai' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $timestamp, new DateTimeZone( 'Asia/Kolkata' ) ) );
}
/**
* @param string $type Evidence event type.
* @return string
*/
private static function event_label( $type ) {
$labels = array(
'consent_collected' => __( 'Visitor consent decision', 'consiva-ai' ),
'consent_withdrawn' => __( 'Visitor withdrew consent', 'consiva-ai' ),
'consent_config_published' => __( 'Consent settings published', 'consiva-ai' ),
'scan_completed' => __( 'Site scan completed', 'consiva-ai' ),
'enforcement_verified' => __( 'Verification passed', 'consiva-ai' ),
'enforcement_conflict' => __( 'Verification found a conflict', 'consiva-ai' ),
'recommendation_applied' => __( 'Recommendation applied', 'consiva-ai' ),
'form_consent_collected' => __( 'Form consent recorded', 'consiva-ai' ),
);
return isset( $labels[ $type ] ) ? $labels[ $type ] : ucfirst( str_replace( '_', ' ', $type ) );
}
/**
* @param string $action Nonce action.
* @return void
*/
private static function guard( $action ) {
if ( ! Capabilities::can_manage() ) { // §9.1: consiva_manage_privacy (administrators hold it).
wp_die( esc_html__( 'You do not have permission to manage Consiva evidence.', 'consiva-ai' ), '', array( 'response' => 403 ) );
}
check_admin_referer( $action );
}
/**
* @param WP_Error|null $error Error, or null on success.
* @param string $message Success message.
* @param string $request_id X-Request-Id.
* @return void
*/
private static function finish( $error, $message = '', $request_id = '' ) {
if ( $error instanceof WP_Error ) {
Notices::push_error( $error );
} else {
Notices::push( 'success', $message, $request_id );
}
wp_safe_redirect( Screens::url() . '#consiva-consent-export' );
exit;
}
}