<?php
/**
 * Please see wp-cookie-allow.php for more details.
 *
 * @author $Vincent Weber <weepie-plugins@outlook.com>$
 */

use WpieFw\Helpers\WpieMiscHelper;
use WpieFw\Templates\WpieTemplate;
use WpieFw\Templates\Files\WpieTemplatesFileFinder;
use WpieFw\Exceptions\WpieExceptionLogger;
use WpieFw\Exceptions\WpieRuntimeException;
use WpieFw\Exceptions\WpieInvalidArgumentException;
use WpieFw\Exceptions\WpieExceptionInterface;

if( ! defined( 'ABSPATH' ) ) exit;

if( !class_exists( 'WpcaModuleConsentLog' ) ) {
	/**
	 * WpcaLog Class
	 *
	 * Module to handle the consent logging functionality
	 *
	 * @author $Vincent Weber <weepie-plugins@outlook.com>$
	 *
	 * @since 3.2
	 */
	class WpcaModuleConsentLog extends WpieFw\Modules\Iterator\WpieModule
	{
		/**
		 * The required mysql version
		 *
		 * Older mysql version don't support ipv6
		 *
		 * @var string
		 */
		const REQUIRED_MYSQL_VERSION = '5.6.3';

		/**
		 * Table column id
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_COL_ID = 'id';

		/**
		 * Table column IP address
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_COL_IP_ADDRESS = 'ip_address';

		/**
		 * Table column IP address alias
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_COL_IP_ADDRESS_ALIAS = 'ip';

		/**
		 * Table column consent
		 *
		 * @since 3.2
		 *
		 * @var unknown
		 */
		const TABLE_COL_CONSENT = 'consent';

		/**
		 * Table column created
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_COL_CREATED = 'created';

		/**
		 * Table column updated
		 *
		 * @since 3.2
		 *
		 * @var unknown
		 */
		const TABLE_COL_UPDATED = 'updated';

		/**
		 * Prefix for the cookie catgory columns
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_COL_CC_PREFIX = 'cc_';

		/**
		 * Table name logs
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_NAME_LOGS = 'wpca_logs';

		/**
		 * Table name logs legacy
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TABLE_NAME_LOGS_LEGACY = 'wpca_logs_legacy';

		/**
		 * The default query limit
		 *
		 * @since 3.2
		 *
		 * @var integer
		 */
		const SQL_DEFAULT_BULK_LIMIT = 1000;

		/**
		 * The default order by column
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const SQL_DEFAULT_ORDERBY_COL = 'id';

		/**
		 * The default query order
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const SQL_DEFAULT_ORDER = 'desc';

		/**
		 * File name for the JavaScript
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const FILE_NAME_FRONTEND_JS = 'wpca-consent-log';

		/**
		 * File name for the JavaScript
		 *
		 * @since 3.2.3
		 *
		 * @var string
		 */
		const FILE_NAME_SETTINGS_JS = 'wpca-consent-log-settings';

		/**
		 * File name for the settings CSS
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const FILE_NAME_SETTINGS_CSS = 'wpca-consent-log';

		/**
		 * Context for creating
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const DB_CONTEXT_CREATE = 'create';

		/**
		 * Context for reading
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const DB_CONTEXT_READ = 'read';

		/**
		 * Template file name of the logs list table
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_LIST_TABLE = 'wpca-log-list-table.php';

		/**
		 * Template file name of the delete log button
		 *
		 * @since 3.3
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_BTN_DEL_LOG = 'wpca-btn-delete-log.php';

		/**
		 * AJAX action logging consent
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		private $_ajaxActionLog = 'wpca-log-consent';

		/**
		 * AJAX action filter
		 *
		 * @since 3.2.3
		 *
		 * @var string
		 */
		private $_ajaxActionFilter = 'wpca-consent-log-filter';

		/**
		 * AJAX action delete
		 *
		 * @since 3.2.3
		 *
		 * @var string
		 */
		private $_ajaxActionDelete = 'wpca-consent-log-delete';

		/**
		 * AJAX action delete all
		 *
		 * @since 3.2.11
		 *
		 * @var string
		 */
		private $_ajaxActionDeleteAll = 'wpca-consent-log-delete-all';

		/**
		 * AJAX action delete consent by user
		 *
		 * @since 3.3
		 *
		 * @var string
		 */
		private $_ajaxActionDeleteRequestByUser = 'wpca-consent-log-delete-by-user';

		/**
		 * AJAX action delete consent by user
		 *
		 * @since 3.3
		 *
		 * @var string
		 */
		private $_ajaxActionExport = 'wpca-consent-log-export';

		/**
		 * AJAX context
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		private $_ajaxContext = '';

		/**
		 * Flag if system should finally log
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		private $_needLogging = false;

		/**
		 * Flag if IP addresses should be anonymized
		 *
		 * @since 3.2.3
		 *
		 * @var string
		 */
		private $_anonymizeIP = false;

		/**
		 * The columns for the log table
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_columns = array();

		/**
		 * Aliases for columns
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_columnAliases = array();

		/**
		 * The category columns for the log table
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_columnsCc = array();

		/**
		 * The allowed Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_cookieCategories = array();

		/**
		 * @since 3.2
		 *
		 * @var string
		 */
		private $_tableName = '';

		/**
		 * These columns are generated during runtime
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_runtimeGeneratedColumns = array();

		/**
		 * Flag that legacy logi is needed
		 *
		 * @var boolean
		 */
		private $_needLegacyLogic = false;

		/**
		 * Flag that legacy logic should be done
		 *
		 * @var boolean
		 */
		private $_doLegacyLogic = false;

		/**
		 * Constructor
		 *
		 * @since 3.2
		 */
		public function __construct()
		{
			global $wpdb;

			$this->setPriority( 11 );

			$mysqlVersion = $wpdb->get_var( 'SELECT VERSION()' );

			if( $wpdb->is_mysql && version_compare( $mysqlVersion, self::REQUIRED_MYSQL_VERSION, '<'  ) ) {
				$this->_needLegacyLogic = true;
				add_filter( 'wpca_consent_log_do_legacy_logic', '__return_true' );
			}

			/**
			 * Let others flag if lagacy logic is needed
			 *
			 * @since 3.2
			 *
			 * @var boolean
			 */
			$this->_needLegacyLogic = apply_filters( 'wpca_consent_log_need_legacy_logic', $this->_needLegacyLogic );

			/**
			 * Flag if lagacy logic should be done
			 *
			 * @since 3.2
			 *
			 * @var boolean
			 */
			$this->_doLegacyLogic = apply_filters( 'wpca_consent_log_do_legacy_logic', false );

			if( $this->isLegacyLogicNeeded() ) {
				$this->_tableName = $wpdb->prefix.self::TABLE_NAME_LOGS_LEGACY;
			} else {
				$this->_tableName = $wpdb->prefix.self::TABLE_NAME_LOGS;
			}

			$this->_ajaxContext = md5( 'wpc@_logg3r' );

			$this->_columnAliases[self::TABLE_COL_IP_ADDRESS] = self::TABLE_COL_IP_ADDRESS_ALIAS;

			// exclude these column form being validated from the client
			$this->_runtimeGeneratedColumns[] = self::TABLE_COL_ID;
			$this->_runtimeGeneratedColumns[] = self::TABLE_COL_IP_ADDRESS;
			$this->_runtimeGeneratedColumns[] = self::TABLE_COL_CREATED;
			$this->_runtimeGeneratedColumns[] = self::TABLE_COL_UPDATED;
		}

		/**
		 * {@inheritDoc}
		 * @see WpieFw\Modules\Iterator\WpieModule::start()
		 */
		public function start() {}

		/**
		 * Get the table name
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function getTableName()
		{
			return $this->_tableName;
		}

		/**
		 * Callback for the wpca_settings_scripts_after hook
		 *
		 * Enqueue jQuery scripts for the admin settings page
		 *
		 * @access public
		 *
		 * @param string $hook_suffix
		 * @param WP_Scripts $wp_scripts
		 * @param bool $isScriptDebug
		 *
		 * @uses wp_enqueue_script()
		 */
		public function setScriptsSettings( $hook_suffix, $wp_scripts, $isScriptDebug )
		{
			$ext = ( $isScriptDebug ) ? '.js' : '.min.js';
			wp_enqueue_script( 'wpca-log',  $this->globals->get( 'moduleUri' ) . '/'. parent::getIndex() .'/js/' . self::FILE_NAME_SETTINGS_JS . $ext, array( 'jquery', 'wpie-settings' ), $this->globals->get( 'version' ) );
		}

		/**
		 * Callback for the wpca_settings_scripts_l10n_data hook
		 *
		 * @param array $l10nData
		 *
		 * @since 3.2.3
		 *
		 * @return array
		 */
		public function setScriptSettingsl10nData( $l10nData )
		{
			$l10nData['log_filter_not_possible'] = __( 'Filtering on column "%COL%" is not possible.', 'wpca' );
			$l10nData['log_filter_not_possible_value_max'] = __( 'Filtering on column "%COL%" is not possible. Your value can be maximal: %MAX%.', 'wpca' );
			$l10nData['log_filter_no_results'] = __( 'No results found.', 'wpca' );
			$l10nData['log_filter_del_confirm'] = __( 'Are you sure you want to delete this log?', 'wpca' );
			$l10nData['log_filter_del_all_confirm'] = __( 'Are you sure you want to delete all logs?', 'wpca' );
			$l10nData['log_export_confirm'] = __( 'Are you sure you want to export all logs?', 'wpca' );
			$l10nData['log_export_no_results'] = __( 'No consent logs found to export.', 'wpca' );

			return $l10nData;
		}

		/**
		 * Callback for the wpca_settings_styles hook
		 *
		 * Set the styles for the admin settings page
		 *
		 * @access public
		 *
		 * @param WP_Styles $wp_styles
		 * @param bool $isScriptDebug
		 *
		 * @since 3.2
		 */
		public function setStylesSettings( $wp_styles, $isScriptDebug )
		{
			$ext = ( $isScriptDebug ) ? '.css' : '.min.css';
			$relModulePath = parent::getIndex() . '/css/' . self::FILE_NAME_SETTINGS_CSS . $ext;

			$path = $this->globals->get( 'moduleUri' ) . '/' . $relModulePath;

			wp_enqueue_style( 'wpca-log', $path, array(), $this->globals->get( 'version' ) );
		}

		/**
		 * Callback for the wpca_scripts_frontend hook
		 *
		 * @access public
		 *
		 * @param array $scripts
		 *
		 * @since 3.2
		 */
		public function setScripts( $scripts )
		{
			$relModulePath = parent::getIndex() . '/js/' . self::FILE_NAME_FRONTEND_JS;
			$path = $this->globals->get( 'moduleUri' ) . '/' . $relModulePath;

			$script = new stdClass();
			$script->path = $path;
			$script->dep = [ WpcaModuleFrontend::FILE_NAME_JS ]; // handle from WpcaModuleFrontend::setScripts()
			$script->args = [ 'in_footer' => true ];

			$scripts[] = $script;

			return $scripts;

		}


		/**
		 * Callback for the wpca_script_frontend_vars_before hook
		 *
		 * @access public
		 *
		 * @param array $vars
		 *
		 * @since 3.2
		 * @since 3.4.5 renamed to setScriptVars
		 *
		 * @return array
		 */
		public function setScriptVars( $vars )
		{
			$vars['ajaxContextConsentLog'] = $this->_ajaxContext;
			$vars['needLogging'] = $this->_needLogging;

			return $vars;
		}


		/**
		 * Callback for the wpca_templ_vars hook
		 *
		 * Add the table HTML for the log list
		 *
		 * @param array	$vars
		 * @param string $tab
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function setTemplateVars( $vars, $tab )
		{
			// if not on content tab, do not go further
			if( 'consent_log' !== $tab ) {
				return $vars;
			}

			try {
				$table = $this->_renderTable();
			} catch ( WpieExceptionInterface $e ) {
				throw $e;
			}

			$vars['table'] = $table;

			return $vars;
		}

		/**
		 * Callback for the wpca_registered_shortcodes hook
		 *
		 * Register the shortcodes for this module
		 *
		 * @param array $registered
		 *
		 * @since 3.3
		 *
		 * @return array
		 */
		public function registerShortcodes( $registered = array() )
		{
			$shordcodes = array(
					'wpca_consent_log_delete' => 'doShortcodeBtnDeleteLog'
			);

			foreach ( $shordcodes as $shordcode => $callback ) {
				$registered[] = $shordcode;

				if( !$this->_needLogging || !defined( 'WPCA_ACTIVE' ) ) {
					$callback = array( 'WpieFw\Helpers\WpieMiscHelper', 'doShortcodeNot' );
				} else {
					$callback = array( $this, $callback );
				}

				add_filter( 'wpca_shortcode_' . $shordcode, $callback, 10, 3 );
			}

			return $registered;
		}

		/**
		 * Callback for the wpca_shortcode_wpca_cookie_policy_link hook
		 *
		 * Render the shortcode content
		 *
		 * @access public
		 *
		 * @param array $atts
		 * @param string $content
		 * @param string $tag
		 *
		 * @uses self::_renderBtnDeleteLog()
		 *
		 * @since 3.3
		 *
		 * @return string
		 */
		public function doShortcodeBtnDeleteLog( $atts = array(), $content = '', $tag = '' )
		{
			$atts = shortcode_atts(
				array(
						'txt' => '',
						'txt_processing' => '',
				), $atts, $tag );

			$txt = $atts['txt'];
			$txtProcessing = $atts['txt_processing'];

			return $this->_renderBtnDeleteLog( $txt, $txtProcessing );
		}

		/**
		 * Callback for the wpca_activate_plugin hook
		 *
		 * @uses self::_createTable()
		 *
		 * @since 3.2
		 */
		public function activatePlugin( $upgrading )
		{
			$this->_createTable();
		}

		/**
		 * Callback for the wpca_validate_ajax_data_{self::_ajaxContext} hook
		 *
		 * Validate the AJAX request data
		 *
		 * @param array $validateReturn
		 * @param string $action
		 * @param array $data
		 *
		 * @uses self::_getColumnsForCategories()
		 * @uses self::_validateInsertData()
		 *
		 * @return array
		 */
		public function validateAjaxData( $validateReturn, $action, $data )
		{
			try {
				if( $action !== $this->_ajaxActionLog &&
				    $action !== $this->_ajaxActionDeleteRequestByUser ) {
					$validateReturn['is_valid'] = false;
					$validateReturn['msg'] = __( 'Invalid AJAX request context.' );
					return $validateReturn;
				}

				if(  $action === $this->_ajaxActionLog ) {
					if( !isset( $data['consent'] ) ) {
						$validateReturn['is_valid'] = false;
						$validateReturn['msg'] = __( 'Required AJAX request data missing: consent.' );
						return $validateReturn;
					} elseif( !isset( $data['categories'] ) || !is_array( $data['categories'] ) ) {
						$validateReturn['is_valid'] = false;
						$validateReturn['msg'] = __( 'Required AJAX request data missing or invalid: categories.' );
						return $validateReturn;
					}

					// get the category columsn names for the passed categories
					$ccColumns = $this->_getColumnsForCategories( $data['categories'] );
					$data = $data+$ccColumns;

					// validate the data. An Exception is thrown if not valid
					$this->_validateInsertData( $data );
				}

				if(  $action === $this->_ajaxActionDeleteRequestByUser ) {
					// no specific checks needed at the moment
				}
			} catch ( WpieExceptionInterface $e ) {
				$validateReturn['is_valid'] = false;
				$validateReturn['msg'] = __( 'Invalid AJAX request data: ' . $e->getMessage() );
			}

			return $validateReturn;
		}

		/**
		 * Callback for the wpca_ajax_json_return hook
		 *
		 * @param array $return
		 * @param string $action
		 * @param array $data
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function process( $return, $action, $data )
		{
			try {
				// if action does not match one of the following, return directly
			    if( $action !== $this->_ajaxActionLog &&
				    $action !== $this->_ajaxActionFilter &&
					$action !== $this->_ajaxActionDelete &&
					$action !== $this->_ajaxActionDeleteAll &&
					$action !== $this->_ajaxActionDeleteRequestByUser &&
					$action !== $this->_ajaxActionExport ) {
					return $return;
				}

				switch ( $action ) {
					case $this->_ajaxActionLog:
						$return = $this->_processLog( $return, $data );
						break;
					case $this->_ajaxActionFilter:
						$return = $this->_processFilter( $return, $data );
						break;
					case $this->_ajaxActionDelete:
						$return = $this->_processDelete( $return, $data );
						break;
					case $this->_ajaxActionDeleteAll:
					    $return = $this->_processDeleteAll( $return, $data );
					    break;
					case $this->_ajaxActionDeleteRequestByUser:
						$return = $this->_processDeleteRequestByUser( $return, $data );
						break;
					case $this->_ajaxActionExport:
						$return = $this->_processExport( $return, $data );
						break;
				}

				return $return;
			} catch( WpieExceptionInterface $e ) {
				throw $e;
			}
		}

		/**
		 * Determine if logging is needed
		 *
		 * Look at the "log_enable" setting first, then if user is logged in
		 *
		 * @since 3.2
		 *
		 * @return boolean
		 */
		public function isLoggingNeeded()
		{
			$needed = $this->settingsProcessor
				->get( 'consent_log' )
				->get( 'log_enable' );

			if( is_user_logged_in() ) {
				$needed = false;
			}

			/**
			 * Let others determine if logging is needed
			 *
			 * @since 3.2
			 *
			 * @var boolean $needed
			 */
			return apply_filters( 'wpca_consent_log_needed', $needed );
		}

		/**
		 * Helper to check if the legacy logic is needed
		 *
		 * @since 3.2
		 *
		 * @return boolean
		 */
		public function isLegacyLogicNeeded()
		{
			return ( $this->_needLegacyLogic && $this->_doLegacyLogic );
		}

		/**
		 * Callback for the wpca_init_frontend_only_hooks hook
		 *
		 * @since 3.2
		 */
		public function initHooksFrontend()
		{
			if( !$this->_needLogging = $this->isLoggingNeeded() || !defined( 'WPCA_ACTIVE' ) ) {
				return;
			}

			add_filter( 'wpca_script_frontend_vars_before', array( $this, 'setScriptVars' ) );
			add_filter( 'wpca_scripts_frontend', array( $this, 'setScripts' ) );
		}


		/**
		 * {@inheritDoc}
		 * @see \WpieFw\Modules\Iterator\WpieModuleInterface::init()
		 */
		public function init()
		{
			// return to prevent fatal errors
			// @see https://github.com/webRtistik/wp-cookie-allow/issues/79
			/** @var WpcaModuleAutomate $moduleAutomate */
			if( false === ( $moduleAutomate = $this->getModule( 'automate' ) ) ) {
				return;
			}

			/** @var WpcaModuleCore $moduleCore */
			if( false === ( $moduleCore = $this->getModule( 'core' ) ) ) {
				return;
			}

			$currentTabIdx = $this->settingsProcessor->settingsPage->currentTabIdx;

			// flag if IP's should be anonymized
			$this->_anonymizeIP = $this->settingsProcessor
				->get( 'consent_log' )
				->get( 'log_enable_anonymize_ip' );

			if( $this->settingsProcessor->settingsPage->onSettingsPage ) {
				if( $this->_needLegacyLogic && !$this->_isTableCreated() ) {
					// diable logging
					add_filter( 'wpca_log_needed', '__return_false' );
					// show a admin notice
					$this->notice( sprintf( __( 'Consent Logging needs MySql version %s or higher. To create a legacy table, add the following code to your functions.php and deactivate/activate the plugin. <code>%s</code>', 'wpca' ),
						self::REQUIRED_MYSQL_VERSION,
						'add_filter( \'wpca_consent_log_do_legacy_logic\', \'__return_true\' );'
						), 'error' );
				} elseif( !$this->_isTableCreated() ) {
					// show a admin notice
					$this->notice( __( 'It seems that the Consent Logging database table hasn\'t been created. Please deactivate/activate the plugin.', 'wpca' ), 'error' );
				}

				// show an admin notice if wp_privacy_anonymize_ip() function not exists
				if( $this->_anonymizeIP && !function_exists( 'wp_privacy_anonymize_ip' ) ) {
					$this->notice( sprintf( __( 'Consent Logging could not anonymize IP Addresses. Please <a href="%s" target="_blank">update your WordPress core</a> to at least 4.9.6.', 'wpca' ),
						admin_url( 'update-core.php' )
						), 'error' );
				}
			}

			if( is_admin() ) {
				add_action( 'wpca_activate_plugin', array( $this, 'activatePlugin' ) );

				if( 'consent_log' === $currentTabIdx ) {
					add_action( 'wpca_settings_scripts_after', array( $this, 'setScriptsSettings' ), 10, 3 );
					add_action( 'wpca_settings_styles', array( $this, 'setStylesSettings' ), 10, 2 );
					add_filter( 'wpca_templ_vars', array( $this, 'setTemplateVars' ), 10, 2 );
					add_filter( 'wpca_settings_scripts_l10n_data', array( $this, 'setScriptSettingsl10nData' ), 10, 2 );
				}

				if( $moduleCore->doingWpcaAjaxAction ) {
					add_filter( 'wpca_validate_ajax_data_' . $this->_ajaxContext, array( $this, 'validateAjaxData' ), 10, 3 );
					add_filter( 'wpca_ajax_json_return', array( $this,'process' ), 10, 3 );
				}
			}

			// register shortcodes for both admin (ajax) requests and frontend
			add_filter( 'wpca_registered_shortcodes', array( $this, 'registerShortcodes' ) );

			// apply hooks for frontend only
			add_action( 'wpca_init_frontend_only_hooks', array( $this, 'initHooksFrontend') );

			$this->_cookieCategories = $moduleAutomate->mapCookieCategoryConfig();

			$this->_columnsCc = array();
			foreach ( $this->_cookieCategories as $idx => $category ) {
				$this->_columnsCc[] = $this->_getColumnNameForCategory( $idx );
			}

			$this->_columns = array(
					self::TABLE_COL_ID,
					self::TABLE_COL_IP_ADDRESS,
					self::TABLE_COL_CONSENT );

			// add the category columns
			$this->_columns = array_merge( $this->_columns, $this->_columnsCc, array( self::TABLE_COL_CREATED, self::TABLE_COL_UPDATED ) );

			// create test record
			if( $this->settingsProcessor->settingsPage->onSettingsPage && $this->_isTableCreated() && isset( $_REQUEST['wpca_log_consent_test'] ) ) {
				$testNumber = absint( $_REQUEST['wpca_log_consent_test'] );
				$testNumber = ( 0 < $testNumber && 9999 > $testNumber ) ? $testNumber : 1;
				try {
					$this->_testCreate( $testNumber );
					$this->notice( __( 'Test records created.', 'wpca' ), 'notice' );
				} catch( WpieExceptionInterface $e ) {
					WpieExceptionLogger::log( $e, true );
					$this->notice( __( 'Could not create test records because an error occured.', 'wpca' ) );
				}
			}
		}

		/**
		 * Process the AJAX logging
		 *
		 * @param array $return
		 * @param array $data
		 *
		 * @since 3.2.3
		 *
		 * @return array
		 */
		private function _processLog( $return, $data )
		{
			// construct the data based on the payload
			// get the category columsn names for the passed categories
			$ccColumns = $this->_getColumnsForCategories( $data['categories'] );
			$data = $data+$ccColumns;

			// add the data to the return array
			// set $validate to false as the data has been validate already
			$id = $this->_create( $data, false );
			$return = ( $id ) ? true : false;

			return $return;
		}

		/**
		 * Process the AJAX filter
		 *
		 * @param array $return
		 * @param array $data
		 *
		 * @uses self::_readAllForSettings()
		 * @uses self::_readFor()
		 *
		 * @since 3.2.3
		 *
		 * @return array
		 */
		private function _processFilter( $return, $data )
		{
			$needle = $data['ndl'];
			$column = $data['col'];
			$return['count'] = 0;
			$return['table'] = array();

			if( !$this->_isValidColumn( $column ) ) {
				throw new WpieInvalidArgumentException( __( sprintf( 'filter column not valid. valid values are: %s', join( ', ', $this->_columns ) ), 'wpca' ) );
			}

			$needleCharFirst = $needle[0];
			$needleCharLast = $needle[strlen($needle)-1];
			$hasLikeOperator = ( '%' === $needleCharFirst || '%' === $needleCharLast );

			// read form the DB
			if( '*' === $needle ) {
				$rows = $this->_readAllForSettings();
			} elseif( $hasLikeOperator ) {
				$rows = $this->_readFor( $needle, $column, 'like' );
			} else {
				$rows = $this->_readFor( $needle, $column );
			}

			// if no errors occured
			// render the table if at least 1 row is fetched
			if( false !== $rows ) {
				$count = count( $rows );
				if( 0 < $count ) {
					$return['count'] = $count;
					$return['table'] = $this->_renderTable( $rows );
				}
			}

			return $return;
		}

		/**
		 * Process the AJAX Delete
		 *
		 * @param array $return
		 * @param array $data
		 *
		 * @uses self::_delete()
		 *
		 * @since 3.2.3
		 *
		 * @return array
		 */
		private function _processDelete( $return, $data )
		{
			$id = (int) $data['id'];
			$return['deleted'] = false;

			$this->_delete( $id );
			$return['deleted'] = $id;

			return $return;
		}

		/**
		 * Process the AJAX Delete
		 *
		 * @param array $return
		 * @param array $data
		 *
		 * @uses self::_deleteAll()
		 *
		 * @since 3.2.11
		 *
		 * @return array
		 */
		private function _processDeleteAll( $return, $data )
		{
		    $return['deleted'] = 0;
		    $return['table'] = array();

		    $this->_deleteAll();

		    // read form the DB
		    $rows = $this->_readAll( array( 'limit' => 0 ) );

		    // if no errors occured
		    // render the table, also with zero rows after delete all
		    if( false === $rows ) {
		        $return['deleted'] = 1;
		    }

		    $return['table'] = $this->_renderTable( $rows );

		    return $return;
		}

		/**
		 * Process the AJAX Delete request by user
		 *
		 * @param array $return
		 * @param array $data
		 *
		 * @since 3.3
		 *
		 * @return array
		 */
		private function _processDeleteRequestByUser( $return, $data )
		{
		    $return['deleted'] = 0;

			// get the users IP address
			$ipAddress = WpieMiscHelper::getClientIp();

			// first try to find a record on the full IP address
			$rows = $this->_readFor( $ipAddress );

			// when no result, try finding the anonymized address
			if( function_exists( 'wp_privacy_anonymize_ip' ) ) {
				$ipAddress = wp_privacy_anonymize_ip( $ipAddress );
				$rowsAno = $this->_readFor( $ipAddress );

				if( false !== $rowsAno ) {
					$rows = array_merge( $rows, $rowsAno );
				}
			}

			if( false !== $rows ) {
				$count = count( $rows );
				if( 0 < $count ) {
					foreach( $rows as $row ) {
						$deleted = $this->_delete( $row->id );
						if( $deleted ) {
							$return['deleted']++;
						}
					}
				}
			}

		    return $return;
		}

		/**
		 * Process the AJAX export
		 *
		 * @param array $return
		 * @param array $data
		 *
		 * @uses self::_deleteAll()
		 *
		 * @since 3.3
		 *
		 * @return array
		 */
		private function _processExport( $return, $data )
		{
		    $return['exported'] = 0;
			$return['data'] = '';

		    // read form the DB
		    $rows = $this->_readAll( array( 'limit' => 0 ) );

		    // if no errors occured
		    if( false !== $rows ) {
		        $return['exported'] = count( $rows );
				$return['data'] = $this->_getCsvDataForRows( $rows );
				$return['filename'] = sprintf( 'wpca-consent-log-export-%s.csv', date( 'Y-m-d', time() ) );
		    }

		    return $return;
		}

		/**
		 * Get a comma separated CSV string for the export
		 *
		 * @param array the rows from the database
		 *
		 * @since 3.3
		 * @return string
		 */
		private function _getCsvDataForRows( $rows = [] ) {
			$linesStr = "";
			$linesArr[] = join( ",", array_map( function( $col ) {
				return str_replace( ',', '',$col );
			}, $this->_getColumnTitles() ) );

			foreach( $rows as $row ) {
				$lineArr =[];
				foreach( $this->_columns as $col ) {
					$lineArr[] = $row->{$col};
				}
				$lineStr = join( ",", $lineArr );
				$linesArr[] = $lineStr;
			}

			$linesStr = join( "\n", $linesArr );
			return $linesStr;
		}

		/**
		 * Get column titles
		 *
		 * @since 3.3
		 * @return array
		 */
		private function _getColumnTitles()
		{
			$columns = array();
			foreach( $this->_columns as $column ) {
				if( $this->_isCcColumn( $column ) ) {

					$columns[$column] = $this->_cookieCategories[$this->_getCategoryForColumnName( $column )]['label'];
				} else {
					$columns[$column] = ucfirst( str_replace( '_', ' ', $column ) );
				}
			}

			return $columns;
		}

		/**
		 * Helper to convert a category name into column name
		 *
		 * @param string $category
		 *
		 * @since 3.2
		 *
		 * @return string the prefixed category name
		 */
		private function _getColumnNameForCategory( $category = '' )
		{
			if( 0 === strpos( $category, self::TABLE_COL_CC_PREFIX ) ) {
				return $category;
			}

			return str_replace( '-', '_', self::TABLE_COL_CC_PREFIX.$category );
		}

		/**
		 * Helper to convert a category column name into category
		 *
		 * @param string $category
		 *
		 * @since 3.2
		 *
		 * @return string|boolean the prefixed category name or false on failure
		 */
		private function _getCategoryForColumnName( $columnName = '' )
		{
			// if is already a category, return $columnName
			if( in_array( $columnName, array_keys( $this->_cookieCategories ) ) ) {
				return $columnName;
			}

			if( 0 !== strpos( $columnName, self::TABLE_COL_CC_PREFIX ) ) {
				return false;
			}

			return str_replace( '_', '-', str_replace( self::TABLE_COL_CC_PREFIX, '', $columnName ) );
		}

		/**
		 * Get column category data for given categories array
		 *
		 * An array indexed by cookie category column name and value 0 or 1
		 *
		 * @param array $categories
		 * @return array
		 */
		private function _getColumnsForCategories( $categories = array() )
		{
			if( empty( $categories ) ) {
				return array();
			}

			$columns = array();
			foreach ( $this->_columnsCc as $columnName ) {
				$category = $this->_getCategoryForColumnName( $columnName );
				$columns[$columnName] = ( in_array( $category, $categories ) ) ? 1 : 0;
			}

			return $columns;
		}

		/**
		 * Get the insert data array
		 *
		 * @param array $data
		 *
		 * @uses WpieMiscHelper::getClientIp()
		 * @uses esc_sql()
		 * @uses wp_privacy_anonymize_ip()
		 *
		 * @since 3.2
		 * @since 3.2.3 added wp_privacy_anonymize_ip() call
		 *
		 * @return array
		 */
		private function _getInsertData( $data )
		{
			// if ip_address is not set, try to resolve it
			if( !isset( $data[self::TABLE_COL_IP_ADDRESS] ) ) {
				$data[self::TABLE_COL_IP_ADDRESS] = WpieMiscHelper::getClientIp();
			}

			extract( $data );

			$dateNow = current_time( 'mysql' );
			$dateNowStr = "'$dateNow'";

			// get the packed representatation of the passed string format
			// to store as VARBINARY(16)
			// $ipAddress = $this->_humanReadableToPackedIp( $ip_address );

			if( $this->_anonymizeIP && function_exists( 'wp_privacy_anonymize_ip' ) ) {
				$ip_address = wp_privacy_anonymize_ip( $ip_address );
			}

			if( !$this->isLegacyLogicNeeded() ) {
				$row['ip_address'] = "INET6_ATON('".esc_sql( $ip_address )."')";
			} else {
				$row['ip_address'] = "'$ip_address'";
			}

			$row['consent'] = $consent;
			$row['created'] = $dateNowStr;
			$row['updated'] = $dateNowStr;

			foreach ( $this->_columnsCc as $column ) {
				$row[$column] = ${$column};
			}

			return $row;
		}

		/**
		 * Get the columns for inserting in a query
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		private function _getSelectColumnsStr()
		{
			$select = join( ',', $this->_columns );

			if( !$this->isLegacyLogicNeeded() ) {
				$select = str_replace( self::TABLE_COL_IP_ADDRESS, 'INET6_NTOA(`'.self::TABLE_COL_IP_ADDRESS.'`) as '.self::TABLE_COL_IP_ADDRESS, $select );
			}

			return $select;
		}

		/**
		 * Determine if passed column is a Cookie category column
		 *
		 * @param string $column
		 *
		 * @since 3.2
		 *
		 * @return boolean
		 */
		private function _isCcColumn( $column = '' )
		{
			$column = trim( $column );

			if( false !== ( $category  = $this->_getCategoryForColumnName( $column ) ) ) {
				return in_array( $category, array_keys( $this->_cookieCategories ) );
			}

			return false;
		}

		/**
		 * Validate the given column name
		 *
		 * @param string $column
		 *
		 * @uses trim()
		 *
		 * @since 3.2
		 *
		 * @return string|boolean the column if valid, else false
		 */
		private function _isValidColumn( $column = '' )
		{
			$column = trim( $column );

			return ( in_array( $column, $this->_columns ) || in_array( $column, $this->_columnAliases ) ) ? $column : false;
		}

		/**
		 * Validate the given value for column name
		 *
		 * @param mixed $value
		 * @param string $column
		 * @param string $context
		 * @param string $comparison
		 *
		 * @uses esc_sql()
		 * @uses trim()
		 *
		 * @throws InvalidArgumentException
		 *
		 * @since 3.2
		 *
		 * @return void
		 */
		private function _isValidColumnValue( &$value = null, &$column = '', $context = 'create', $comparison = '=' )
		{
			$valid = true;
			$msg = $column . ' is not valid.';
			$creating = ( self::DB_CONTEXT_CREATE === $context );
			$reading = ( self::DB_CONTEXT_READ === $context );

			if( false === ( $column = $this->_isValidColumn( $column ) ) ) {
				$valid = false;
			}

			if( $valid ) {
				if( is_string( $value ) ) {
					$value = trim( $value );
				}

				switch ( $column ) {
					case self::TABLE_COL_ID:
						if( $reading && !(int)$value ) {
							$valid = false;
							$msg = $value . ' is not a valid id.';
						}
						break;
					case self::TABLE_COL_IP_ADDRESS:
					case self::TABLE_COL_IP_ADDRESS_ALIAS:
						// set ip alias to real column name
						if( self::TABLE_COL_IP_ADDRESS_ALIAS === $column ) {
							$column = self::TABLE_COL_IP_ADDRESS;
						}

						if( $reading && '=' === $comparison ) {
							// validate the IP address
							if( !WpieMiscHelper::isValidIpAddress( $value ) ) {
								$valid = false;
								$msg = $value . ' is not a valid IP address.';
							}
						}
						break;
					case self::TABLE_COL_CONSENT:
						// validate the consent value
						if( 0 !== (int)$value && 1 !== (int)$value ) {
							$valid = false;
							$msg = $value . ' is not a valid consent value. Must be 0 or 1.';
						}
						break;
					case self::TABLE_COL_CREATED:
					case self::TABLE_COL_UPDATED:

						// @todo for reading, needs validation. for now skip

						break;
					case $this->_isCcColumn( $column ):
						// validate the cookie categories value
						if( 0 !== (int)$value && 1 !== (int)$value ) {
							$valid = false;
							$cat = $this->_getCategoryForColumnName( $column );
							$msg = $value . ' is not a valid cookie category value for "'.$cat.'". Must be 0 or 1.';
						}
						break;
					default:
						// other column not supported
						$valid = false;
						break;
				}
			}

			if( $creating && !$valid ) {
				throw new WpieInvalidArgumentException( $msg );
			}
		}

		/**
		 * Check if the legacy table has been created
		 *
		 * @uses wpdb::query()
		 *
		 * @since 3.2
		 *
		 * @return boolean
		 */
		private function _isTableCreated()
		{
			static $created = null;

			if( null !== $created ) {
				return $created;
			}

			global $wpdb;

			$tableName = $this->getTableName();
			$result = $wpdb->query( "SHOW TABLES LIKE '$tableName';" );

			$created = ( false !== $result && 0 < $result  );

			return $created;
		}

		/**
		 * Validate the insert data
		 *
		 * @param array $data
		 *
		 * @uses self::_isValidColumnValue()
		 *
		 * @throws InvalidArgumentException
		 *
		 * @since 3.2
		 */
		private function _validateInsertData( $data )
		{
			try {
				foreach ( $this->_columns as $column ) {
					if( in_array( $column, $this->_runtimeGeneratedColumns ) ) {
						continue;
					}
					if( !isset( $data[$column] ) ) {
						throw new WpieInvalidArgumentException( __( sprintf( 'data for the "%s" column not set.', $column ), 'wpca' ) );
					}
				}

				extract( $data );

				foreach ( $this->_columns as $column ) {
					if( in_array( $column, $this->_runtimeGeneratedColumns ) ) {
						continue;
					}
					$value = ${$column};
					$this->_isValidColumnValue( $value, $column );
				}
			} catch ( WpieExceptionInterface $e ) {
				throw $e;
			}
		}

		/**
		 * Gets a Packed ip address string
		 *
		 * @param string $address
		 *
		 * @uses inet_pton()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		private function _humanReadableToPackedIp( $address = '' )
		{
			$packed = inet_pton( $address );

			return $packed;
		}

		/**
		 * Gets a human readable ip address string
		 *
		 * @param string $packed
		 *
		 * @uses inet_ntop()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		private function _packedToHumanReadableIp( $packed = '' )
		{
			$address = inet_ntop( $packed );

			return $address;
		}

		/**
		 * Create the DB table
		 *
		 * @since 3.2
		 */
		private function _createTable()
		{
			global $wpdb;

			$charsetCollate = $wpdb->get_charset_collate();

			$tableName = $this->getTableName();
			$tableType = 'VARBINARY(16)';

			if( $this->isLegacyLogicNeeded() ) {
				$tableType = 'VARCHAR(45)';
			}

			$ccCol = $idxCcCol = array();
			foreach ( $this->_cookieCategories as $idx => $category ) {
				$catColumnName = $this->_getColumnNameForCategory( $idx );
				$default = ( $category === WpcaModuleAutomate::CC_IDX_FUNCTIONAL ) ? "1" : "0";
				// create a string defining the cookie category column
				$ccCol[] = "`$catColumnName` tinyint(1) DEFAULT $default NOT NULL";
				// create a string defining the index on the cookie category column
				$idxCcCol[] ="INDEX `idx_$catColumnName` (`$catColumnName`)";
			}
			$ccColStr = join( ', ', $ccCol );
			$idxCcColStr = join( ', ', $idxCcCol );

			$colId = self::TABLE_COL_ID;
			$colIpaddress = self::TABLE_COL_IP_ADDRESS;
			$colConsent = self::TABLE_COL_CONSENT;
			$colCreated = self::TABLE_COL_CREATED;
			$colUpdated = self::TABLE_COL_UPDATED;

			$sql = "CREATE TABLE IF NOT EXISTS `$tableName` (
			`$colId` int NOT NULL AUTO_INCREMENT,
			`$colIpaddress` $tableType NOT NULL,
			`$colConsent` tinyint(1) NOT NULL,
			$ccColStr,
			`$colCreated` datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
			`$colUpdated` datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
			PRIMARY KEY (`id`),
			INDEX `idx_ip_address` (`ip_address`),
			$idxCcColStr
			) $charsetCollate;";

			require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
			dbDelta( $sql );
		}

		/**
		 * Create a log entry
		 *
		 * @param array $data
		 *
		 * @uses self::_validateInsertData()
		 * @uses self::_getInsertData()
		 * @uses wpdb::insert()
		 *
		 * @throws InvalidArgumentException
		 *
		 * @since 3.2
		 *
		 * @return int|boolean the inserted ID or false
		 */
		private function _create( $data, $validate = true )
		{
			if( $validate ) {
				// validate the $data passed.
				// if not valid, an InvalidArgumentException is thrown
				$this->_validateInsertData( $data );
			}

			global $wpdb;

			$tableName = $this->getTableName();
			$rowData = $this->_getInsertData( $data );

			$sql = "INSERT INTO `$tableName` (";
			$sql .= "`" .join( '`,`', array_keys( $rowData ) ) . "`";
			$sql .= ") VALUES(";
			$sql .= join( ',', $rowData );
			$sql .= ");";

			$wpdb->suppress_errors = true;
			$wpdb->query( $sql );
			$wpdb->suppress_errors = false;

			if( '' !== $wpdb->last_error ) {
				throw new WpieRuntimeException( $wpdb->last_error );
			}

			return ( $wpdb->insert_id ) ? $wpdb->insert_id : false;
		}

		/**
		 * Create a test record
		 *
		 * @uses self::_getInsertData()
		 * @uses self::_create()
		 *
		 * @since 3.2
		 *
		 * @param number $number
		 */
		private function _testCreate( $number = 1 )
		{
			// first truncate the table
			$this->_deleteAll();

			$ipTemplate = '111.%d.222.%d';

			$data = array();
			foreach ( $this->_columnsCc as $columnName ) {
				$data[$columnName] = 1;
			}
			for( $i=0; $i<$number; $i++ ) {
				$data[self::TABLE_COL_IP_ADDRESS] = sprintf( $ipTemplate, rand(1, 111), rand(1, 111) );
				$data[self::TABLE_COL_CONSENT] = rand(0, 1);
				$rowData = $this->_getInsertData( $data );
				$this->_create( $data, false );
			}
		}

		/**
		 * Read a log entry by id
		 *
		 * @param number $id
		 *
		 * @since 3.2
		 *
		 * @return object|boolean false on failure
		 */
		private function _read( $id = 0 )
		{
			try {
				$id = (int) $id;
				$column = self::TABLE_COL_ID;

				// throws an Exception when not valid
				$this->_isValidColumnValue( $id, $column, self::DB_CONTEXT_READ );

				global $wpdb;

				$select = $this->_getSelectColumnsStr();
				$sql = $wpdb->prepare( "SELECT $select FROM {$this->getTableName()} WHERE id = %d LIMIT 1;", array( $id ) );

				$wpdb->suppress_errors = true;
				$row = $wpdb->get_row( $sql );
				$wpdb->suppress_errors = false;

				if( '' !== $wpdb->last_error ) {
					throw new WpieRuntimeException( $wpdb->last_error );
				}
				if( null !== $row ) {
					return $row;
				}

				return false;
			} catch( WpieExceptionInterface $e ) {
				throw $e;
			}
		}

		/**
		 * Read a log entry by column and value
		 *
		 * @param mixed $value
		 * @param string $column
		 * @param string $comparison
		 *
		 * @uses self::_isValidColumn()
		 * @uses wpdb::get_row()
		 *
		 * @since 3.2
		 *
		 * @return array|boolean false on failure
		 */
		private function _readFor( $value = null, $column = 'ip', $comparison = '=' )
		{
			try {
				// throws an Exception when not valid
				$this->_isValidColumnValue( $value, $column, self::DB_CONTEXT_READ, $comparison );

				$select = $this->_getSelectColumnsStr();
				$valuePlaceholder = ( $column === self::TABLE_COL_ID ) ? '%d' : '%s';

				if( !$this->isLegacyLogicNeeded() && self::TABLE_COL_IP_ADDRESS === $column ) {
				    switch ( $comparison ) {
				        case 'like':
				            // Like queries on the varbinary(16) ip address column, needs this to work
				            // and the default $valuePlaceholder %s instead of INET6_ATON(%s)
				            $column = "INET6_NTOA(`$column`)";
				            break;
				        case '=':
				        default:
				            $valuePlaceholder = "INET6_ATON(%s)";
				            break;
				    }
				}

				// perform a LIKE or = query
				switch ( $comparison ) {
					case 'like':
						$sql = "SELECT $select FROM {$this->getTableName()} WHERE $column LIKE($valuePlaceholder);";
						break;
					case '=':
					default:
						$sql = "SELECT $select FROM {$this->getTableName()} WHERE $column = $valuePlaceholder;";
						break;
				}

				global $wpdb;
				$sql = $wpdb->prepare( $sql, array( $value ) );

				$wpdb->suppress_errors = true;
				$rows = $wpdb->get_results( $sql );
				$wpdb->suppress_errors = false;

				if( '' !== $wpdb->last_error ) {
					throw new WpieRuntimeException( $wpdb->last_error );
				}
				if( null !== $rows ) {
					return $rows;
				}

				return false;
			} catch( WpieExceptionInterface $e ) {
				throw $e;
			}
		}

		/**
		 * Read all log entries
		 *
		 * @param string|int $limit
		 * @param string $orderby
		 *
		 * @uses self::_getSelectColumnsStr()
		 * @uses esc_sql()
		 * @uses wpdb::get_results()
		 *
		 * @since 3.2
		 *
		 * @return array|bool false on failure
		 */
		private function _readAll( $args = array() )
		{
			global $wpdb;

			$select = $this->_getSelectColumnsStr();

			$args = wp_parse_args( $args, array(
				'limit' => self::SQL_DEFAULT_BULK_LIMIT,
				'orderbyColumn' => self::SQL_DEFAULT_ORDERBY_COL,
				'order' => self::SQL_DEFAULT_ORDER
			) );

			/**
			 * Let others modify the read all query args
			 *
			 * @since 3.2
			 *
			 * @var array $args
			 */
			$args = apply_filters( 'wpca_read_all_args', $args );

			// @TODO is this escaping needed?
			$args = esc_sql( $args );

			extract( $args );

			$order = strtoupper( $order );

			if( 0 < (int)$limit ) {
				$limit = (int)$limit;
			} else {
				$limit = self::SQL_DEFAULT_BULK_LIMIT;
			}
			if( !in_array( $orderbyColumn, $this->_columns ) ) {
				$orderbyColumn = self::SQL_DEFAULT_ORDERBY_COL;
			}
			if( !in_array( $order, array( 'ASC', 'DESC' ) ) ) {
				$order = self::SQL_DEFAULT_ORDER;
			}

			$sql = "SELECT $select FROM {$this->getTableName()} ORDER BY $orderbyColumn $order LIMIT $limit;";

			$wpdb->suppress_errors = true;
			$rows = $wpdb->get_results( $sql );
			$wpdb->suppress_errors = false;

			if( '' !== $wpdb->last_error ) {
				throw new WpieRuntimeException( $wpdb->last_error );
			}

			if( null !== $rows && !empty( $rows ) ) {
				return $rows;
			}

			return false;
		}

		/**
		 * Read all log entries for and apply admin settings
		 *
		 * @uses self::_readAll()
		 *
		 * @since 3.2.3
		 *
		 * @return object|boolean
		 */
		private function _readAllForSettings()
		{
			$limit = $this->settingsProcessor
				->get( 'consent_log' )
				->get( 'log_limit' );

			$order = $this->settingsProcessor
				->get( 'consent_log' )
				->get( 'log_order' );

			return $this->_readAll( array( 'limit' => $limit, 'order' => $order ) );
		}

		/**
		 * Update a log entry
		 *
		 * @since 3.2
		 */
		private function _update()
		{

		}

		/**
		 * Delete a log entry
		 *
		 * @param int $id
		 *
		 * @uses self::_isValidColumnValue()
		 * @uses wpdb::delete()
		 *
		 * @throws InvalidArgumentException
		 *
		 * @since 3.2
		 *
		 * @return int|bool The number of rows deleted, or false on failure
		 */
		private function _delete( $id = 0 )
		{
			try {
				$id = (int) $id;
				$column = self::TABLE_COL_ID;

				// throws an Exception when not valid
				$this->_isValidColumnValue( $id, $column, self::DB_CONTEXT_READ );

				global $wpdb;

				$wpdb->suppress_errors = true;
				$deleted = $wpdb->delete( $this->getTableName(), array( $column => $id ), array( '%d' ) );
				$wpdb->suppress_errors = false;

				if( '' !== $wpdb->last_error ) {
					throw new WpieRuntimeException( $wpdb->last_error );
				}

				return $deleted;
			} catch ( WpieExceptionInterface $e ) {
				throw $e;
			}
		}

		/**
		 * Delete all log entry
		 *
		 * @since 3.2.3
		 */
		private function _deleteAll()
		{
			global $wpdb;

			$wpdb->suppress_errors = true;
			$wpdb->query( "TRUNCATE {$this->getTableName()};" );
			$wpdb->suppress_errors = false;

			if( '' !== $wpdb->last_error ) {
				throw new WpieRuntimeException( $wpdb->last_error );
			}
		}

		/**
		 * Render the delete log button
		 *
		 * @access 	public
		 *
		 * @param string $txt
		 * @param string $txtProcessing
		 * @param bool $echo
		 *
		 * @uses WpieTemplate
		 *
		 * @since 3.3
		 *
		 * @return string the rendered html if $echo is bool false
		 */
		private function _renderBtnDeleteLog( $txt = '', $txtProcessing = '', $echo = false )
		{
			if( '' === $txt ) {
				$txt = $this->settingsProcessor->get('content')->get( 'consent_log_button_delete_log_txt' );
			}
			if( '' === $txtProcessing ) {
				$txtProcessing = $this->settingsProcessor->get('content')->get( 'consent_log_button_delete_log_txt_processing' );
			}

			try {
				$finder = new WpieTemplatesFileFinder( dirname(__FILE__) . '/templates', '', '', self::TEMPL_FILE_NAME_BTN_DEL_LOG, false );
				$btn = new WpieTemplate( $finder, self::TEMPL_FILE_NAME_BTN_DEL_LOG );
				$btn->setVar( 'txt', $txt );
				$btn->setVar( 'txt_processing', $txtProcessing );
				$html = $btn->render( false, true );
			} catch( \Throwable $e ) {
				// log only when logged in
				WpieExceptionLogger::log( $e, true );
				// on frontend do not show message
				$html = '';
			}

			if( $echo ) {
				echo $html;
			}	else {
				return $html;
			}
		}

		/**
		 * Render the log table
		 *
		 * @param array $logs
		 *
		 * @uses self::_readAllForSettings()
		 *
		 * @since 3.2
		 * @since 3.2.3 added param $logs
		 *
		 * @return string
		 */
		private function _renderTable( $logs = array() )
		{
			// query the logs
			$columns = $this->_getColumnTitles();

			// Alter the columns and apply some default modifications
			add_filter( 'wpca_log_list_table_columns', function( $columns ) {
				unset( $columns[self::TABLE_COL_ID] );
				unset( $columns[self::TABLE_COL_UPDATED] );
				$columns[self::TABLE_COL_IP_ADDRESS] = str_replace( 'Ip', 'IP', $columns[self::TABLE_COL_IP_ADDRESS] );

				return $columns;
			} );

			/**
			 * Let other modify the column in the list table
			 *
			 * @since 3.2
			 *
			 * @var array $columns
			 */
			$columns = apply_filters( 'wpca_log_list_table_columns', $columns );

			if( empty( $logs ) ) {
				$logs = $this->_readAllForSettings();
			}

			$hasLogs = ( false !== $logs );
			$count = ( $hasLogs ) ? count( $logs ) : 0;
			$hasScroll = ( 17 < $count );

			try {
				$finder = new WpieTemplatesFileFinder( dirname(__FILE__) . '/templates', '', '', self::TEMPL_FILE_NAME_LIST_TABLE, false );
				$template = new WpieTemplate( $finder, self::TEMPL_FILE_NAME_LIST_TABLE );
			} catch( WpieExceptionInterface $e ) {
				throw $e;
			}

			$template->setVar( 'columns', $columns );
			$template->setVar( 'columns_count', count( $columns ) );
			$template->setVar( 'logs', $logs );
			$template->setVar( 'has_logs', $hasLogs );
			$template->setVar( 'has_scroll', $hasScroll );

			return $template->render( false, true );
		}
	}
}