Consent Record Log With Shortened IP Addresses
Records each cookie consent choice (accepted or rejected, categories, date and privacy policy version) in its own database table, with only a shortened IP address, and lists the latest records under Tools > Consent Log.
Opens WPIDE on
Code
// Days to keep consent records.
$wpide_consent_keep_days = 395;
$wpide_consent_table = function () {
global $wpdb;
return $wpdb->prefix . 'wpide_consent_log';
};
add_action( 'admin_init', function () use ( $wpide_consent_table ) {
global $wpdb;
if ( '1' === get_option( 'wpide_consent_log_db' ) ) {
return;
}
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( 'CREATE TABLE ' . $wpide_consent_table() . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
created datetime NOT NULL,
choice varchar(20) NOT NULL,
categories varchar(190) NOT NULL DEFAULT '',
ip varchar(45) NOT NULL DEFAULT '',
policy varchar(32) NOT NULL DEFAULT '',
PRIMARY KEY (id),
KEY created (created)
) " . $wpdb->get_charset_collate() . ';' );
update_option( 'wpide_consent_log_db', '1' );
} );
add_action( 'rest_api_init', function () use ( $wpide_consent_table ) {
register_rest_route( 'wpide/v1', '/consent', array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'args' => array(
'choice' => array( 'type' => 'string', 'enum' => array( 'granted', 'denied', 'partial' ), 'required' => true ),
'categories' => array( 'type' => 'string', 'default' => '' ),
),
'callback' => function ( WP_REST_Request $request ) use ( $wpide_consent_table ) {
global $wpdb;
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? wp_privacy_anonymize_ip( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : '';
$rate = 'wpide_consent_rate_' . md5( $ip );
if ( (int) get_transient( $rate ) >= 10 ) {
return new WP_REST_Response( array( 'ok' => false ), 429 );
}
set_transient( $rate, (int) get_transient( $rate ) + 1, HOUR_IN_SECONDS );
if ( '1' !== get_option( 'wpide_consent_log_db' ) ) {
return new WP_REST_Response( array( 'ok' => false ), 503 );
}
$policy_id = (int) get_option( 'wp_page_for_privacy_policy' );
$wpdb->insert( $wpide_consent_table(), array(
'created' => current_time( 'mysql', true ),
'choice' => $request['choice'],
'categories' => substr( sanitize_text_field( $request['categories'] ), 0, 190 ),
'ip' => $ip,
'policy' => $policy_id ? (string) get_post_modified_time( 'Y-m-d', true, $policy_id ) : '',
) );
return array( 'ok' => true );
},
) );
} );
add_action( 'wp_footer', function () {
?>
<script>
(function () {
var url = <?php echo wp_json_encode( esc_url_raw( rest_url( 'wpide/v1/consent' ) ) ); ?>;
var timer;
function send(choice, categories) {
clearTimeout(timer);
timer = setTimeout(function () {
var body = new URLSearchParams({ choice: choice, categories: categories || '' });
if (navigator.sendBeacon) { navigator.sendBeacon(url, body); } else { fetch(url, { method: 'POST', body: body, keepalive: true }); }
}, 500);
}
document.addEventListener('wpide:consent', function (e) { send(e.detail === 'granted' ? 'granted' : 'denied', 'all'); });
document.addEventListener('wp_listen_for_consent_change', function (e) {
var detail = e.detail || {};
var cats = Object.keys(detail);
var granted = cats.filter(function (c) { return detail[c] === 'allow' || detail[c] === 'granted'; });
send(granted.length === cats.length ? 'granted' : (granted.length ? 'partial' : 'denied'), granted.join(','));
});
})();
</script>
<?php
}, 99 );
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'wpide_consent_log_cleanup' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wpide_consent_log_cleanup' );
}
} );
add_action( 'wpide_consent_log_cleanup', function () use ( $wpide_consent_table, $wpide_consent_keep_days ) {
global $wpdb;
if ( '1' === get_option( 'wpide_consent_log_db' ) ) {
$wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpide_consent_table() . ' WHERE created < %s LIMIT 5000', gmdate( 'Y-m-d H:i:s', time() - $wpide_consent_keep_days * DAY_IN_SECONDS ) ) );
}
} );
add_action( 'admin_menu', function () use ( $wpide_consent_table ) {
add_management_page( 'Consent Log', 'Consent Log', 'manage_options', 'wpide-consent-log', function () use ( $wpide_consent_table ) {
global $wpdb;
$rows = '1' === get_option( 'wpide_consent_log_db' ) ? $wpdb->get_results( 'SELECT * FROM ' . $wpide_consent_table() . ' ORDER BY id DESC LIMIT 200' ) : array();
echo '<div class="wrap"><h1>Consent Log</h1><p>The latest 200 consent choices. IP addresses are stored shortened.</p>';
echo '<table class="widefat striped"><thead><tr><th>Date (UTC)</th><th>Choice</th><th>Categories</th><th>IP</th><th>Policy version</th></tr></thead><tbody>';
if ( ! $rows ) {
echo '<tr><td colspan="5">No records yet.</td></tr>';
}
foreach ( $rows as $row ) {
printf( '<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>', esc_html( $row->created ), esc_html( $row->choice ), esc_html( $row->categories ), esc_html( $row->ip ), esc_html( $row->policy ) );
}
echo '</tbody></table></div>';
} );
} );
Notes
Picks up choices from the WP Consent API and from the wpide:consent event of the consent banner snippet; records older than $wpide_consent_keep_days are removed daily.
How to add this snippet
- Install WPIDE on your WordPress site (it is free) and open Code Snippets.
- Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
- It is added inactive: review the code and settings, then switch it on.