Halloween Sale 15% off Ends in See the plans
PHP

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.

Don’t have WPIDE yet? Install it on your site

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

  1. Install WPIDE on your WordPress site (it is free) and open Code Snippets.
  2. Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
  3. It is added inactive: review the code and settings, then switch it on.

Have a snippet worth sharing?

Publish it to the library from WPIDE or right here. Sign in with any WordPress site that runs WPIDE, free or Pro. Public snippets are reviewed by the WPIDE team before they appear.

Which site should get this snippet?

Enter the address of your WordPress site. WPIDE opens there in a new tab with the snippet ready to review and save.

Remembered in this browser only, so you are asked once.

Report this snippet

Tell the WPIDE team what is wrong: broken code, something harmful, spam or copied work.