Created
April 11, 2018 08:19
-
-
Save aristath/f037ff2aa84ed69fd4a49b639fc4c2d0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* GDPR-Compliance class. | |
* | |
* @since 1.0 | |
* @author Aristeides Stathopoulos @aristath | |
*/ | |
/** | |
* The Fusion_CS_GDPR object. | |
* | |
* @since 1.0 | |
*/ | |
class GDPR_Consent_Logger { | |
/** | |
* The option-name we'll be using to store our data. | |
* | |
* @access private | |
* @since 1.0 | |
* @var string | |
*/ | |
private $option_name; | |
/** | |
* Constructor. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $option_name The name of the option that will be handling our data. | |
*/ | |
public function __construct( $option_name ) { | |
$this->option_name = $option_name; | |
} | |
/** | |
* Stores GDPR data. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $email The user's email. | |
* @param string $key The key we'll use to store this data. | |
* @param mixed $data The data to store. | |
* @return void | |
*/ | |
public function set( $email = '', $key = '', $data = array() ) { | |
// Early exit if we don't have everything we need. | |
if ( ! $email || ! $key || empty( $data ) ) { | |
return; | |
} | |
// Get the existing data. | |
$data = $this->get( $email ); | |
// Check if we have data for this user or not. | |
if ( ! isset( $data[ $email ] ) ) { | |
$data[ $email ] = array(); | |
} | |
// Update the data. | |
$data[ $email ][ $key ] = $data; | |
update_option( $this->option_name, $data ); | |
} | |
/** | |
* Merges GDPR data and updates setting. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $email The user's email. | |
* @param string $key The key we'll use to store this data. | |
* @param mixed $data The data to store. | |
* @return array The data, merged - after updating the setting. | |
*/ | |
public function merge_update( $email = '', $key = '', $data = array() ) { | |
// Early exit if we don't have everything we need. | |
if ( ! $email || ! $key || empty( $data ) ) { | |
return; | |
} | |
// Get old data. | |
$old_data = $this->get( $email, $key ); | |
// If old data was an array, merge/replace with new, update the saved value and then return. | |
if ( $old_data && is_array( $old_data ) && is_array( $data ) ) { | |
$data = array_replace_recursive( $old_data, $data ); | |
$this->set( $email, $key, $data ); | |
return $data; | |
} | |
// If we got this far, just replace old with new and return. | |
$this->set( $email, $key, $data ); | |
return $data; | |
} | |
/** | |
* Gets GDPR-related data. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $email The user's email. | |
* @param string $key The key we want to get. Leave empty to get all. | |
* @return mixed The return value-type depends on the $key used. | |
*/ | |
public function get( $email = '', $key = '' ) { | |
// Get the existing data. | |
$data = get_option( $this->option_name, array() ); | |
if ( $key ) { | |
if ( isset( $data[ $key ] ) ) { | |
return $data[ $key ]; | |
} | |
return; | |
} | |
return $data; | |
} | |
/** | |
* Delete all data for an email address. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $email The email address of the user whose data we want to delete. | |
* @param string $key The key we want to delete from the data. If empty delete all. | |
* @param bool $keep_consents If set to true, will force keeping the consents data. | |
* @return void | |
*/ | |
public function delete( $email = '', $key = '', $keep_consents = false ) { | |
// We have a key defined. | |
if ( $key ) { | |
// Get all data for this email. | |
$data = $this->get( $email ); | |
// If there is no data for this key, unset it and save the data anew. | |
if ( isset( $data[ $key ] ) ) { | |
if ( 'consents' === $key && $keep_consents ) { | |
return; | |
} | |
unset( $data[ $key ] ); | |
$this->set( $email, $data ); | |
return; | |
} | |
} | |
// If we got this far, we need to completely delete the email data. | |
$data = get_option( $this->option_name ); | |
if ( isset( $data[ $email ] ) ) { | |
// If we need to keep the consents, delete everything else but keep those. | |
// If there are no consents present, delete completely. | |
if ( $keep_consents ) { | |
$data_to_keep = false; | |
if ( isset( $data[ $email ]['consents'] ) && ! empty( $data[ $email ]['consents'] ) ) { | |
$data_to_keep = $data[ $email ]['consents']; | |
} | |
unset( $data[ $email ] ); | |
if ( $data_to_keep ) { | |
$data[ $email ] = array( | |
'consents' => $data_to_keep, | |
); | |
} | |
update_option( $this->option_name, $data ); | |
return; | |
} | |
// If we got this far we don't want to keep anything. | |
// Just unset and save new value for the option. | |
unset( $data[ $email ] ); | |
update_option( $this->option_name, $data ); | |
} | |
} | |
/** | |
* Logs user consent. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $email The user's email address. | |
* If no email is provided, then tokenize the IP. | |
* @param string $id A unique ID for what the user consents to. | |
* @param string $context Some context for this consent. | |
* @param string $url The URL from where this consent came from. | |
*/ | |
public function log_consent( $email = '', $id = '', $context = '', $url = '' ) { | |
// Early exit if there is no ID. Nothing to save. | |
if ( ! $id ) { | |
return; | |
} | |
// Sanitize the $id. | |
$id = sanitize_key( $id ); | |
// If no email was provided, tokenize an anonymized IP. | |
if ( ! $email ) { | |
$email = md5( $this->get_ip( true ) ); | |
} | |
// Get the user's existing consents. | |
$consents = $this->get( $email, 'consents' ); | |
if ( ! $consents || ! is_array( $consents ) ) { | |
$consents = array(); | |
} | |
// ALlow saving multiple consents for the same ID. | |
if ( ! isset( $consents[ $id ] ) ) { | |
$consents[ $id ] = array(); | |
} | |
// Build the consent. | |
$consent = array( | |
'time' => time(), | |
'ip' => $this->get_ip( true ), | |
); | |
if ( $context ) { | |
$consent['context'] = sanitize_text_field( $consent ); | |
} | |
if ( $url ) { | |
$consent['url'] = esc_url_raw( $url ); | |
} | |
// Add consent to the array of consents. | |
$consents[ $id ][] = $consent; | |
// Save consent. | |
$this->set( $email, 'consents', $consents ); | |
} | |
/** | |
* Gets the client IP. | |
* | |
* @access public | |
* @since 1.0 | |
* @param bool $anonymize If we sant to anonymize the IP or not. | |
* @return string | |
*/ | |
public function get_ip( $anonymize = true ) { | |
$ip = ''; | |
if ( isset( $_SERVER['REMOTE_ADDR'] ) ) { | |
$ip = wp_unslash( $_SERVER['REMOTE_ADDR'] ); // WPCS: sanitization ok. | |
if ( is_string( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: sanitization ok. | |
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); | |
} | |
} | |
if ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { | |
$ip = wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ); // WPCS: sanitization ok. | |
if ( is_string( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) { // WPCS: sanitization ok. | |
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ); | |
} | |
} | |
if ( isset( $_SERVER['HTTP_CLIENT_IP'] ) && ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { | |
$ip = wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ); // WPCS: sanitization ok. | |
if ( is_string( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) ) ) { // WPCS: sanitization ok. | |
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) ); | |
} | |
} | |
if ( ! $ip ) { | |
return false; | |
} | |
if ( $anonymize ) { | |
return $this->anonymize_ip( $ip ); | |
} | |
return $ip; | |
} | |
/** | |
* Sanitizes an IP. | |
* We need to do this to be sure nothing malicious gets in our database, | |
* no matter how smart they are (which is not possible since they're probably smarter than us). | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $ip The IP we want to sanitize. | |
* @param bool $anonymize If we want to anonymize the IP or not. | |
* @return string | |
*/ | |
public function sanitize_ip( $ip, $anonymize = true ) { | |
// If we've got an array of IPs, we need to sanitize them separately. | |
if ( false !== strpos( $ip, ',' ) ) { | |
// Get the parts. | |
$ips = explode( ',', $ip ); | |
// Sanitize each part separately. | |
foreach ( $ips as $key => $ip ) { | |
$ips[ $key ] = $this->sanitize_ip( $ip, $anonymize ); | |
} | |
// Recombine parts. | |
$ips = implode( ',', $ips ); | |
return sanitize_text_field( $ips ); | |
} | |
// Filters the IP. | |
if ( false !== strpos( $ip, ':' ) ) { // IPv6. | |
$ip_filtered = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ); | |
} elseif ( false !== strpos( $ip, ':' ) ) { // IPv4. | |
$ip_filtered = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ); | |
} | |
// Not an IPv4 or IPv6. | |
if ( ! $ip_filtered ) { | |
$ip_filtered = sanitize_text_field( $ip ); | |
if ( ! $ip_filtered ) { | |
return false; | |
} | |
return ( $anonymize ) ? $this->halve( $ip_filtered ) : $ip_filtered; | |
} | |
// Anonymize it. | |
if ( $anonymize ) { | |
$ip_filtered = $this->anonymize_ip( $ip_filtered ); | |
} | |
// Return sanitized. | |
return sanitize_text_field( $ip_filtered ); | |
} | |
/** | |
* Anonymizes an IP and returns just part of it. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $ip The IP we want to anonymize. | |
*/ | |
public function anonymize_ip( $ip ) { | |
if ( false !== strpos( $ip, ':' ) ) { // IPv6. | |
/** | |
* The IPv6 structure is: | |
* FP | TLA | RES | NLA | SLA | Interface ID. | |
* We need to anonymize more parts than in IPv4 to ensure anonymity. | |
*/ | |
$parts = explode( ':', $ip ); | |
// If we've got too few parts, there's something wrong with the format. | |
// Let's just strip half of it. | |
if ( 6 < count( $parts ) ) { | |
return $this->halved( $ip ); | |
} | |
if ( 3 >= count( $parts ) ) { // Already anonymized. | |
return $ip; | |
} | |
return "{$parts[0]}:{$parts[1]}:{$parts[2]}"; | |
} | |
// If we got this far, it's an IPv4. | |
$parts = explode( '.', $ip ); | |
if ( 4 === count( $parts ) ) { | |
return "{$parts[0]}.{$parts[1]}.{$parts[2]}"; | |
} | |
// Already anonymized. | |
if ( 7 < strlen( $ip ) ) { | |
return $ip; | |
} | |
// If we got this far we don't know what it is. | |
// Just halve it to be sure. | |
return $this->halve( $ip ); | |
} | |
/** | |
* Returns half a string. | |
* | |
* @access public | |
* @since 1.0 | |
* @param string $val The string we hant to halve. | |
* @return string | |
*/ | |
public function halve( $val ) { | |
$halved = ''; | |
$chars = str_split( $val ); | |
$count = count( $chars ); | |
$half = floor( $count ); | |
$i = 0; | |
while ( $i < $half ) { | |
$halved .= $chars[ $i ]; | |
$i++; | |
} | |
return $halved; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment