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

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

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

if( !class_exists( 'WpcaModuleAutomate' ) ) {

	/**
	 * WpcaAutomate Class
	 *
	 * Module to handle all automate processes
	 *
	 * @author $Vincent Weber <weepie-plugins@outlook.com>$
	 *
	 * @since 2.2.7
	 */
	class WpcaModuleAutomate extends WpieFw\Modules\Iterator\WpieModule {

		/**
		 * The current automate action
		 *
		 * @since 2.2.7
		 *
		 * @var string
		 */
		public $action = null;


		/**
		 * Flag if currently erasing
		 *
		 * @since 2.2.7
		 *
		 * @var bool
		 */
		public $erasing = false;


		/**
		 * Flag if currently refreshing
		 *
		 * @since 2.2.7
		 *
		 * @var bool
		 */
		public $refreshing = false;


		/**
		 * Regular exprssion parts for reusing
		 *
		 * @since 2.3
		 *
		 * @var array
		 */
		private $_regexParts;

		/**
		 * regex to match a script tag
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternScriptBasic;

		/**
		 * regex to match an iFrame tag
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternIframeBasic;

		/**
		 * Regex to match a script opening tag
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternScriptTagOpen;

		/**
		 * Regex to match a script opening tag
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternScriptTagClose;

		/**
		 * regex to match a script tag and inner content
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternScriptAllAdvanced;
		/**
		 * regex to match a script tag and inner content with a certain needle
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternScriptHasNeedle;

		/**
		 * regex to match all script src="" tags
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternScriptSrc;

		/**
		 * regex to match an iframe tag for src attribute needle
		 *
		 * @since 3.1.2
		 *
		 * @var string
		 */
		private $_regexPatternIframe;


		/**
		 * regex to match a HTML tag with given attribute name value
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		private $_regexPatternHtmlElemWithAttr;


		/**
		 * Allowed local regex location types
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_regexAllowedLocalLocations = array( 'plugin' => 'wp-content/plugins', 'theme' => 'wp-content/themes' );


		/**
		 * Allowed regex source (url) attributes
		 *
		 * @since 3.2.3
		 *
		 * @var array
		 */
		private $_regexAllowedSourceAttributes = array( 'src', 'href' );


		/**
		 * Flag if third parties has been checked
		 *
		 * @since 2.3
		 *
		 * @var bool
		 */
		private $_hasChecked = false;


		/**
		 * The current 3rd party data while looping
		 *
		 * @since 3.2.15
		 * @var array
		 */
		private $_currentData = null;

		/**
		 * Current automate thirdparty during automating
		 *
		 * @since 2.3
		 *
		 * @var string
		 */
		private $_currentType = null;


		/**
		 * Current thirdparty cookie category during automating
		 *
		 * @since 2.3
		 *
		 * @var string
		 */
		private $_currentCc = null;


		/**
		 * Current thirdparty placeholder text during automating
		 *
		 * @since 3.2.5
		 *
		 * @var string
		 */
		private $_currentTxt = '';


		/**
		 * The cookie category during automating with block all iFrames mode on
		 *
		 * @var string
		 *
		 * @since 3.2.5
		 */
		private $_eraseAllCc = null;


		/**
		 * The Cookie Category names
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_cookieCategories;


		/**
		 * The allowed Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_allowedCookieCategories;


		/**
		 * The required Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @var array
		 */
		private $_requiredCookieCategories;

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

		/**
		 *
		 * Flag if client has Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		private $_hasClientCookieCategories = false;

		/**
		 * Replace pairs
		 *
		 * String to replace with a placeholder
		 * before processing the HTML
		 *
		 * @since 3.3
		 */
		private $_replacePairs = [
			"\r\n" => '_RNL_',
			"\n" => '_NL_',
			'<' => '_LT_'
		];

		/**
		 * 
		 * Flag if blocking YouTube videos in privacy mode
		 * 
		 * @since 3.4.5
		 * 
		 * @var boolean
		 */
		private $_blockYouTubeNoCookie = false;

		/**
		 * File name for the Automate settings JavaScript
		 *
		 * @since 2.3
		 *
		 * @var string
		 */
		const FILE_NAME_AUTOMATE_SETTINGS_JS = 'wpca-automate';


		/**
		 * Default callback string for auto wrapping
		 *
		 * @since 2.3
		 *
		 * @var string
		 */
		const DEFAULT_AUTOMATE_CB_STR = '_automateDefault';


		/**
		 * Google Analytics array key
		 *
		 * @since
		 *
		 * @var string
		 */
		const AR_KEY_GA = 'googleanalytics';


		/**
		 * Gtag.js array key prefix
		 *
		 * @since 3.2.13
		 *
		 * @var string
		 */
		const AR_KEY_GTAG_PREFIX = 'google_gtag_';


		/**
		 * File name for the settings CSS
		 *
		 * @since 3.1
		 *
		 * @var string
		 */
		const FILE_NAME_CSS = 'wpca-automate';


		/**
		 * Template file name of the automate 3rd party list table
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_3RD_PARTY_LIST_TABLE = 'wpca-general-automate-table.php';


		/**
		 * Template file name of the cookie category settings box
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_CC_SETT_BOX = 'wpca-cc-settings-box.php';


		/**
		 * Template file name of the cookie category settings options
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_CC_SETT_OPTIONS = 'wpca-cc-settings-options.php';


		/**
		 * Template file name of the cookie category settings button default settings
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_CC_SETTINGS_BTN_ACCEPT_ALL = 'wpca-cc-settings-btn-accept-all.php';


		/**
		 * Template file name of the cookie category settings button save settings
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_CC_SETTINGS_BTN_SAVE_SETT = 'wpca-cc-settings-btn-save-sett.php';


		/**
		 * Template file name of the Cookie Category settings editable fields
		 *
		 * @since 3.2.11
		 *
		 * @var string
		 */
		const TEMPL_FILE_NAME_CC_EDITABLE_FIELDS = 'wpca-content-cc-editable-fields.php';


		/**
		 * Cookie Category index for functional
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_IDX_FUNCTIONAL = 'functional';


		/**
		 * Cookie Category index for analytical
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_IDX_ANALYTICAL = 'analytical';


		/**
		 * Cookie Category index for social media
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_IDX_SOCIAL_MEDIA = 'social-media';


		/**
		 * Cookie Category index for advertising
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_IDX_ADVERTISING = 'advertising';


		/**
		 * Cookie Category index for other
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_IDX_OTHER = 'other';


		/**
		 * Cookie Category default index
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_IDX_DEFAULT = 'other';


		/**
		 * Cookie Category input name attribute
		 *
		 * @since 3.2
		 *
		 * @var string
		 */
		const CC_ATTR_NAME = 'wpca_cookie_cat';


		/**
		 * Constructor
		 *
		 * @acces	public
		 *
		 * @since	2.2.7
		 */
		public function __construct() {}


		/* (non-PHPdoc)
		 * @see WpiePluginModule::start()
		 */
		public function start()
		{
			$this->_allowedCookieCategories = array(
					self::CC_IDX_FUNCTIONAL,
					self::CC_IDX_ANALYTICAL,
					self::CC_IDX_SOCIAL_MEDIA,
					self::CC_IDX_ADVERTISING,
					self::CC_IDX_OTHER
			);

			// set regex patterns
 			$this->_defineRegex();
		}


		/**
		 * Callback for the wpca_script_frontend_vars_before hook
		 *
		 * @access public
		 *
		 * @param array $vars
		 *
		 * @since 3.2.14
		 * @since 3.4.5 renamed to setScriptVars
		 *
		 * @return array
		 */
		public function setScriptVars( $vars )
		{
			$ccLabels = array_map( function( $data ) {
				return $data['label'];
			}, $this->settingsProcessor->get( 'content' )->get( 'cc_data', $this->getCookieCategories() ) );

			$vars['ccLabels'] = $ccLabels;

			return $vars;
		}

		/**
		 * 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';
			$relModulePath = parent::getIndex() . '/js/' . self::FILE_NAME_AUTOMATE_SETTINGS_JS . $ext;

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

			wp_enqueue_script( 'wpca-automate', $path, array( 'wpca-settings' ), $this->globals->get( 'version' ) );
		}


		/**
		 * 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	2.2.7
		 */
		public function setStylesSettings( $wp_styles, $isScriptDebug )
		{
		    $ext = ( $isScriptDebug ) ? '.css' : '.min.css';
			$relModulePath = parent::getIndex() . '/css/' . self::FILE_NAME_CSS . $ext;

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

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


		/**
		 * Map passed cookie category data with user configured data
		 *
		 * Replaces the default label and description with the settings values in `cc_data`
		 *
		 * @param array $categories
		 *
		 * @since 3.3
		 * @return array
		 */
		public function mapCookieCategoryConfig( $categories = [] ) {
			if( 1 > count( $categories ) ) {
				$categories = $this->_cookieCategories;
			}

			if( $hasCcData = $this->settingsProcessor->get( 'content' )->has( 'cc_data' ) ) {
				$ccData = $this->settingsProcessor->get( 'content' )->get( 'cc_data', array() );
			}

			foreach( $this->_allowedCookieCategories as $idx ) {
				if( $hasCcData && isset( $ccData[$idx] ) && isset( $categories[$idx] )) {
					if( isset( $ccData[$idx]['label'] ) ) {
						$categories[$idx]['label'] = $ccData[$idx]['label'];
					}
					if( isset( $ccData[$idx]['descr'] ) ){
						$categories[$idx]['descr'] = $ccData[$idx]['descr'];
					}
				}
			}

			return $categories;
		}


		/**
		 * Callback for the wpca_templ_vars hook
		 *
		 * Filter the Template vars array and add all variables that need to be available in the settings page Template
		 *
		 * @access	public
		 *
		 * @param 	array	$vars
		 * @param 	string	$tab
		 *
		 * @since 	2.2.7
		 *
		 * @return 	array
		 */
		public function setTemplateVars( $vars, $tab )
		{
			// if not on general tab, do not go further
			if( 'general' !== $tab && 'content' !== $tab ) {
				return $vars;
			}

			try {
			    if( 'general' === $tab ) {
				    $vars['table_automate'] = $this->renderTable();
			    } elseif( 'content' === $tab ) {
			        $vars['categories'] = $this->mapCookieCategoryConfig();
			    }
			} catch ( WpieExceptionInterface $e ) {
				throw $e;
			}

			return $vars;
		}


		/**
		 * Add our <wpca-block> element to the custom_elements array
		 *
		 * @param array $mceInit
		 *
		 * @since 3.2.6
		 *
		 * @return array
		 */
		public function setTinyMceInit( $mceInit = array() )
		{
			if( isset( $mceInit['custom_elements'] ) && !empty( $mceInit['custom_elements'] ) ) {
				$mceInit['custom_elements'] .= ',';
			} else {
				$mceInit['custom_elements'] = '';
			}

			if( isset( $mceInit['extended_valid_elements'] ) && !empty( $mceInit['extended_valid_elements'] ) ) {
				$mceInit['extended_valid_elements'] .= ',';
			} else {
				$mceInit['extended_valid_elements'] = '';
			}

			$mceInit['custom_elements'] .= 'wpca-block';
			$mceInit['extended_valid_elements'] .= 'wpca-block[class|style|data-cc|data-type|data-txt|data-placeholder]';

			return $mceInit;
		}


		/**
		 * Render the 3rd party list table
		 *
		 * @uses self::_getAutomateData()
		 * @uses WpieTemplate::render()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function renderTable()
		{
			// default all parties are checked
			$checkAll = $this->settingsProcessor->get( 'general' )->get( 'automate_check_all' );
			$checkAll = (null == $checkAll) ? 0 : $checkAll;

			$automateData = $this->_getAutomateData();
			// sort the list on label values, so the item index is free to use
			uasort( $automateData, function( $a, $b ) {
				return strcasecmp( $a['label'], $b['label'] );
			} );

			// let others add data to the table
			$extra = array( 'th' => array(), 'td' => array() );
			/**
			 * Let others modify the automate table data
			 *
			 * @var array $extra
			 *
			 * @since 3.2
			 */
			$extra = apply_filters( 'wpca_automate_extra', $extra );

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

			$template->setVar( 'setting', $this->settingsProcessor->getName( 'general' ) );
			$template->setVar( 'automate_data', $automateData );
			$template->setVar( 'automate_check_all', $checkAll );
			$template->setVar( 'automate_request_3rd_mailto', esc_url( 'mailto:weepie-plugins@outlook.com' .
					'?subject=Request for '.$this->globals->get( 'pluginName' ).' missing 3rd party cookie' .
					'&body=Please send us as much information as possible about the missing 3rd party like: an example of an embed code, cookie name(s), product URL, etc. Thanks a lot!' ) );
			$template->setVar( 'extra', $extra );

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


		/**
		 * Render the cookie category settings box
		 *
		 * @uses WpieTemplate::render()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function renderCookieCategorySettingsBox()
		{
			static $counter = 0;
			$counter++;

			$content = wpautop( $this->settingsProcessor->get( 'content' )->get( 'content_cc_txt' ) );
			$shadow = $this->settingsProcessor->get('style')->get( 'style_cc_shadow' );

			// apply other shortcodes
			$content = do_shortcode( $content );

			// ensure allowed html
			$content = wp_kses( $content, wp_kses_allowed_html( WpcaModuleFrontend::KSES_FRONTEND_CONTEXT ) );

			$cssClass = '';
			if( $shadow ) {
				$cssClass .= ' wpca-shadow';
			}

			// render the template
			try {
				$finder = new WpieTemplatesFileFinder( dirname(__FILE__) . '/templates', '', '', self::TEMPL_FILE_NAME_CC_SETT_BOX, false );
				$template = new WpieTemplate( $finder, self::TEMPL_FILE_NAME_CC_SETT_BOX );
			} catch( WpieExceptionInterface $e ) {
				// log only when logged in
				WpieExceptionLogger::log( $e, true );
				// on frontend do not show message
				return '';
			}

			$template->setVar( 'content', $content );
			$template->setVar( 'css_class', $cssClass );
			$template->setVar( 'counter', $counter );

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


		/**
		 * Render the cookie category settings options
		 *
		 * @uses self::getCookieCategories()
		 * @uses WpieTemplate::render()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function renderCookieCategorySettingsOptions()
		{
			static $counter = 0;
			$counter++;

			// get the 'checked' categories
			$categories = $this->getCheckedCookieCategories();

			// Ensure the category for blocking all iFrames is rendered
			if( 'erase-all' === $this->action && !isset( $categories[$this->_eraseAllCc] ) ) {
				$categories[$this->_eraseAllCc] = $this->_cookieCategories[$this->_eraseAllCc];
			}

			$categories = $this->mapCookieCategoryConfig( $categories );

			foreach ( $categories as $k => $cat ) {
			    $categories[$k]['attr'] = array();

				if( $cat['disabled'] ) {
				    $categories[$k]['attr']['disabled'] = 'disabled';
				}
				$categories[$k]['attr']['data-cat'] = $k;
				$categories[$k]['attr']['id'] = "wpca-cookie-cat-$k-$counter";
			}

			// render the template
			try {
				$finder = new WpieTemplatesFileFinder( dirname(__FILE__) . '/templates', '', '', self::TEMPL_FILE_NAME_CC_SETT_OPTIONS, false );
				$template = new WpieTemplate( $finder, self::TEMPL_FILE_NAME_CC_SETT_OPTIONS );
			} catch( WpieExceptionInterface $e ) {
				WpieExceptionLogger::log( $e );
				return '';
			}

			$template->setVar( 'categories', $categories );
			$template->setVar( 'name', self::CC_ATTR_NAME );
			$template->setVar( 'has_cookie_categories', ( 0 < count( $categories ) ) );

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


		/**
		 * Render the cookie category settings button accept all
		 *
		 * @uses WpieTemplate::render()
		 *
		 * @since 3.2.13
		 *
		 * @return string
		 */
		public function renderCookieCategorySettingsBtnAcceptAll()
		{
			$txt = $this->settingsProcessor->get( 'content' )->get( 'content_cc_button_accept_all_txt' );
			$txtProcessing = $this->settingsProcessor->get( 'content' )->get( 'content_cc_button_accept_all_txt_processing' );

			// render the template
			try {
				$finder = new WpieTemplatesFileFinder( dirname(__FILE__) . '/templates', '', '', self::TEMPL_FILE_NAME_CC_SETTINGS_BTN_ACCEPT_ALL, false );
				$template = new WpieTemplate( $finder, self::TEMPL_FILE_NAME_CC_SETTINGS_BTN_ACCEPT_ALL );
			} catch( WpieExceptionInterface $e ) {
				WpieExceptionLogger::log( $e );
				return '';
			}

			$template->setVar( 'txt', $txt );
			$template->setVar( 'txt_processing', $txtProcessing );

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


		/**
		 * Render the cookie category settings button save settings
		 *
		 * @uses self::getCookieCategories()
		 * @uses WpieTemplate::render()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function renderCookieCategorySettingsBtnSave()
		{
			$txt = $this->settingsProcessor->get( 'content' )->get( 'content_cc_button_save_sett_txt' );
			$txtProcessing = $this->settingsProcessor->get( 'content' )->get( 'content_cc_button_save_sett_txt_processing' );

			// render the template
			try {
				$finder = new WpieTemplatesFileFinder( dirname(__FILE__) . '/templates', '', '', self::TEMPL_FILE_NAME_CC_SETTINGS_BTN_SAVE_SETT, false );
				$template = new WpieTemplate( $finder, self::TEMPL_FILE_NAME_CC_SETTINGS_BTN_SAVE_SETT );
			} catch( WpieExceptionInterface $e ) {
				WpieExceptionLogger::log( $e );
				return '';
			}

			$template->setVar( 'txt', $txt );
			$template->setVar( 'txt_processing', $txtProcessing );

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


		/**
		 * Get list of 3rd parties
		 *
		 * @access	public
		 *
		 * @uses	filter wpca_automate_thirparties to allow others to add 3rd parties
		 *
		 * @see WP_oEmbed::providers for a full list of supported Providers
		 *
		 * @since	2.2.7
		 *
		 * @return 	array
		 */
		public function getThirdParties()
		{
			$thirdParties = array();

			// Links and Iframes to embed

			$thirdParties['youtube'] = array(
					'label' 	=> __( 'YouTube', 'wpca' ),
					'js'		=> array(
						'www.youtube.com/iframe_api',
						'wp-content/plugins/elfsight-youtube-gallery-cc/assets/elfsight-youtube-gallery.js'
					),
					'html_elem' => array(
						'name' => 'div',
						'attr' => 'class:elfsight-widget-youtube-gallery'
					),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['vimeo'] = array(
					'label' 	=> __( 'Vimeo', 'wpca' ),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['gmaps'] = array(
					'label' 	=> __( 'Google Maps', 'wpca' ),
					's'			=> 'www.google.com/maps/embed',
					'js'		=> array(
						'maps.googleapis.com',
						'maps.google.com/maps/api/js',
						// Fusion Google Map
						[
							's' => 'wp-content/themes/Avada/includes/lib/assets/min/js/library/infobox_packed.js',
							'dep' => 'google',
							'dt' => 200,
							'r' => 10
						],
						// Map list pro
						[
							's' => 'wp-content/plugins/MapListPro/js/maplistfront.js',
							'dep' => 'google',
							'dt' => 200
						],
						[
							's' => 'wp-content/plugins/MapListPro/js/infobox_packed.js',
							'dep' => 'google',
							'dt' => 200
						]							
					),
					'js_needle' => [
						[
							// General
							's' => 'new google.maps.',
							'dep' => 'google',
							'dt' => 200,
							'r' => 10
						],
						[
							// General
							// Fusion Google Map is also blocked by this
							's' => 'google.maps',
							'dep' => 'google',
							'dt' => 200,
							'r' => 10
						],
						[
							// // WP Google Map Plugin
							's' => 'data("wpgmp_maps',
							'dep' => 'google',
							'dt' => 200,
							'r' => 10
						]
					],
					'html_elem' => [
						[
							// WP Google Map Plugin
							'name' => 'div',
							'attr' => 'class:wpgmp_map'
						],
						[
							// Divi Google Map
							'name' => 'div',
							'attr' => 'class:et_pb_map',
							'regex_suffix' => '(?!_pin)'
						],
						[
							// Fusion Google Map
							'name' => 'div',
							'attr' => 'id:fusion_map_',
						],						
						[
							// Map list pro
							'name' => 'div',
							'attr' => 'class:prettyMapList'
						]
					],
					'cc' => self::CC_IDX_OTHER
			);

			$thirdParties['instagram'] = array(
					'label' 	=> __( 'Instagram', 'wpca' ),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['slideshare'] = array(
					'label' 	=> __( 'Slideshare', 'wpca' ),
					's'			=> 'slideshare.net', // full URL: www.slideshare.net/slideshow/embed_code/key
					'js'		=> false,
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['soundcloud'] = array(
					'label' 	=> __( 'Soundcloud', 'wpca' ),
					's'			=> array( 'soundcloud.com', 'w.soundcloud.com' ), // full URL: w.soundcloud.com/player
					'js'		=> false,
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['twitter'] = array(
					'label' 	=> __( 'Twitter', 'wpca' ),
					's'			=> 'platform.twitter.com',
					'js'		=> 'wp-content/plugins/jetpack/_inc/twitter-timeline.js',
					'js_needle'	=> 'platform.twitter.com',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['googleplus'] = array(
					'label' 	=> __( 'Google+', 'wpca' ),
					's'			=> 'apis.google.com',
					'js'		=> array( 'apis.google.com',
							//'apis.google.com/js/plusone.js',
							//'apis.google.com/js/platform.js',
							//'apis.google.com/js/client:platform.js'
					),
					'je_needle' => array( 'apis.google.com', ' gapi.' ),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties[self::AR_KEY_GA] = array(
					'label' 	=> __( 'Google Analytics', 'wpca' ),
					's'			=> 'google-analytics.com',
					'js'		=> 'www.google-analytics.com/analytics.js',
					'js_needle'	=> array( 'www.google-analytics.com/analytics.js', // new style
							'google-analytics.com/ga.js', // old style
							'window.ga=window.ga', // code needle, new style
							'_getTracker', // code needle, old style)
							'GoogleAnalyticsObject' // Complete Analytics Optimization Suite (CAOS) plugin
							),
					'cc'		=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['facebook'] = array(
					'label' 	=> __( 'Facebook', 'wpca' ),
					's'			=> 'facebook.net',
					'js'		=> array(
						'wp-content/plugins/jetpack/_inc/facebook-embed.js',
						'connect.facebook.net' ), // full URL: connect.facebook.net/en_US/sdk.js
					'js_needle'	=> 'connect.facebook.net',
					'html_elem' => array(
						'name' => 'div',
						'attr' => 'class:fb-'
						),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['facebook_pixel'] = array(
					'label' 	=> __( 'Facebook Pixel Code', 'wpca' ),
					'js'		=> 'connect.facebook.net/en_US/fbevents.js', // full URL: https://connect.facebook.net/en_US/fbevents.js
					'js_needle'	=> array(
							'connect.facebook.net/en_US/fbevents.js',
							'fbq('
						),
					'cc'		=> self::CC_IDX_ANALYTICAL,
					'html_elem'   => array(
						'name' => 'img',
						'attr' => 'src:facebook.com/tr'
					)
			);

			// @see https://developers.facebook.com/docs/messenger-platform/reference/web-plugins/
			$thirdParties['facebook_web_plugins'] = array(
					'label'     => __( 'Facebook Web Plugins', 'wpca' ),
					'js_needle' => 'sdk/xfbml.customerchat.js',
					'html_elem' => array(
							'name' => 'div',
							'attr' => 'class:fb-customerchat'
					),
					'html_elem' => array(
							'name' => 'div',
							'attr' => 'class:fb-messengermessageus'
					),
					'html_elem' => array(
							'name' => 'div',
							'attr' => 'class:fb-send-to-messenger'
					),
					'html_elem' => array(
							'name' => 'div',
							'attr' => 'class:fb-messenger-checkbox'
					),
					'cc'        => self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['linkedin'] = array(
					'label' 	=> __( 'LinkedIn', 'wpca' ),
					's'			=> 'linkedin.com',
					'js'		=> 'platform.linkedin.com', // full URL: platform.linkedin.com/in.js
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['pinterest'] = array(
					'label' 	=> __( 'Pinterest', 'wpca' ),
					's'			=> 'pinterest.com',
					'js'		=> 'assets.pinterest.com', // full URL: assets.pinterest.com/js/pinit.js or assets.pinterest.com/js/pinit_main.js
					'js_needle' => array(
						'window.PinIt',
						'assets.pinterest.com',
						),
					'html_elem' => array(
						'name' => 'a',
						'attr' => 'data-pin-do:embed' // embedBoard, embedPin, embedUser
					),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['pinterest_tag'] = array(
					'label' => __( 'Pinterest tag', 'wpca' ),
					'js' => 's.pinimg.com/ct/core.js',
					'js_needle' => 'pintrk',
					'html_elem' => array(
						'name' => 'img',
						'attr' => 'src:ct.pinterest.com/v3/?tid'
					),
					'cc' => self::CC_IDX_ANALYTICAL
			);

			$thirdParties['disqus'] = array(
					'label' 	=> __( 'Disqus', 'wpca' ),
					's'			=> 'disqus.com',
					'js'		=> 'wp-content/plugins/disqus-comment-system/public/js', // full URL: /wp-content/plugins/disqus-comment-system/public/js/comment_count.js
					'js_needle'	=> 'disqus.com', // full URL: disqus.com/embed.js
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['disqus-comment-system'] = array(
					'label' 	=> __( 'Disqus Comment System', 'wpca' ),
					'js'		=> 'wp-content/plugins/disqus-comment-system/public/js' // full URL: /wp-content/plugins/disqus-comment-system/public/js/comment_count.js
			);

			$thirdParties['addthis'] = array(
					'label' 	=> __( 'AddThis', 'wpca' ),
					's'			=> 'addthis.com',
					'js'		=> 's7.addthis.com', // full URL: s7.addthis.com/js/250/addthis_widget.js"
					'js_needle'	=> array( 'addthis.init', 'addthis_config', 's7.addthis.com' ),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['sharethis'] = array(
					'label' 	=> __( 'Sharethis', 'wpca' ),
					's'			=> 'sharethis.com',
					'js'		=> 'w.sharethis.com', // full UhRL: w.sharethis.com/button/buttons.js
					'js_needle'	=> array( 'stLight.options', 'sharebox' ),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['googleadsense'] = array(
					'label' 	=> __( 'Google Adsense', 'wpca' ),
					'js'		=> 'pagead2.googlesyndication.com', // full URL:pagead2.googlesyndication.com/pagead/show_ads.js
					'js_needle' => array( 'google_ad_client', 'adsbygoogle' ),
					'cc'		=> self::CC_IDX_ADVERTISING,
					'html_elem'   => array(
						'name' => 'ins',
						'attr' => 'class:adsbygoogle'
					)
			);

            $thirdParties[self::AR_KEY_GTAG_PREFIX . self::CC_IDX_ANALYTICAL] = array(
                'label'     => __( 'Google gtag.js (analytical)', 'wpca' ),
                'js' => [
                    'googletagmanager.com/gtag/js?id=UA',
                    'googletagmanager.com/gtag/js?id=G'
                ],
                'js_needle' => array(
                    "gtag('config', 'UA",
                    'gtag("config", "UA',
                    "gtag('config', 'G",
                    'gtag("config", "G',
                    'gtag("event"',
                    "gtag('event'"
                ),
                'cc' => self::CC_IDX_ANALYTICAL
            );

			$thirdParties[self::AR_KEY_GTAG_PREFIX . self::CC_IDX_ADVERTISING] = array(
				'label' 	=> __( 'Google gtag.js (advertising)', 'wpca' ),
                'js' => array(
					'googletagmanager.com/gtag/js?id=AW',
					'googletagmanager.com/gtag/js?id=DC'
				),
                'js_needle' => array(
					"gtag('config', 'AW", 'gtag("config", "AW"',
					"gtag('config', 'DC", 'gtag("config", "DC"',
					'gtag("event"', "gtag('event'"
				),
                'html_elem' => array(
                    'name' => 'img',
                    'attr' => 'src:ad.doubleclick.net/ddm'
				),
				'cc' => self::CC_IDX_ADVERTISING
			);

			$thirdParties['googleadwordsremark'] = array(
					'label' 	=> __( 'Google Adwords remarketing', 'wpca' ),
					'js' 		=> 'googleadservices.com/pagead/conversion.js',
					'js_needle' => array( 'google_conversion_id' ),
					'html_elem' => array(
							array(
									'name' => 'img',
									'attr' => 'src:googleadservices.com/pagead/conversion'
							),
							array(
									'name' => 'img',
									'attr' => 'src:googleads.g.doubleclick.net'
							)
					),
					'cc'		=> self::CC_IDX_ADVERTISING
			);

			$thirdParties['google_recaptcha'] = array(
				'label' => __( 'Google reCAPTCHA', 'wpca' ),
				'js_needle' => [
					'grecaptcha'
				],
				'js' => [
					'www.google.com/recaptcha/api.js',
					'www.recaptcha.net/recaptcha/api.js',
					// Contact Form 7
					// https://wordpress.org/support/topic/compatibility-with-weepie-cookie-allow-plugin/
					// [
					// 	's' => 'wp-content/plugins/contact-form-7/modules/recaptcha/index.js',
					// 	'dep' => 'grecaptcha',
					// 	'dt' => 200
					// ]
				],
				'cc' => self::CC_IDX_OTHER
			);

			// https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce
			$thirdParties[self::AR_KEY_GA . '_ecommerce'] = array(
				'label' => __( 'Google Analytics (ecommerce)', 'wpca' ),
				'js_needle' => array(
					"ga( 'require', 'ec", "ga('require', 'ec",
					"ga( 'ec:", "ga('ec:",
					"ga( 'send', 'pageview", "ga('send', 'pageview"
				),
				'cc' => self::CC_IDX_ANALYTICAL
			);

			$thirdParties['jotform'] = array(
					'label' => __( 'JotForm', 'wpca' ),
					'js'	=> array(
							'form.jotformpro.com/jsform', // full URL: https://form.jotformpro.com/jsform/xyz
							'form.jotform.com/jsform' // full URL: https://form.jotform.com/jsform/xyz
					),
					'cc'	=> self::CC_IDX_OTHER
			);

			$thirdParties['leadpages'] = array(
					'label' => __( 'Leadpages', 'wpca' ),
					'js'    => 'static.leadpages.net/leadboxes/current/embed.js', // full URL: //static.leadpages.net/leadboxes/current/embed.js
					'cc'		=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['cloudflare'] = array(
					'label' 	=> __( 'Cloudflare', 'wpca' ),
					'js_needle' => 'ajax.cloudflare.com', // full //ajax.cloudflare.com/cdn-cgi/nexp/dok3v=85b614c0f6/cloudflare.min.js
					'cc'		=> self::CC_IDX_OTHER
			);

			$thirdParties['bandcamp'] = array(
					'label' 	=> __( 'Bandcamp', 'wpca' ),
					's'			=> 'bandcamp.com/EmbeddedPlayer',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			// block ga code generated by https://wordpress.org/plugins/googleanalytics/
			$thirdParties['sharethis platform api'] = array(
					'label' 	=> __( 'Sharethis platform API', 'wpca' ),
					'js'		=> 'platform-api.sharethis.com/js/sharethis.js',
					'js_needle' => 'googleanalytics_get_script',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['addtoany'] = array(
					'label' 	=> __( 'AddToAny', 'wpca' ),
					'js'		=> 'static.addtoany.com/menu/page.js',
					'js_needle' => array( 'a2a_config', 'a2a.', 'static.addtoany.com' ),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['hupso_share_buttons'] = array(
					'label'     => __( 'Hupso Share Buttons', 'wpca' ),
					'js'        => 'static.hupso.com', // full http://static.hupso.com/share/js/share_toolbar.js
					'js_needle' => 'hupso_services_t',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['matoma'] = array(
					'label'     => __( 'Matomo (Piwik)', 'wpca' ),
					'js_needle' => [ 'var _paq' ], // https://developer.matomo.org/guides/tracking-javascript-guide
					'cc'		=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['matoma_tm'] = array(
				'label'     => __( 'Matomo Tag Manager', 'wpca' ),
				'js_needle' => [ 'var _mtm' ], // https://matomo.org/guide/tag-manager/
				'cc'		=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['amazonadsystem'] = array(
					'label' => __( 'Amazon adsystem', 'wpca' ),
					's'     => '.amazon-adsystem.com',
					'js'    => '.amazon-adsystem.com',
					'cc'	=> self::CC_IDX_ADVERTISING
			);

			/**
			 * @see https://help.activecampaign.com/hc/en-us/articles/115001257184-How-do-I-embed-a-form-on-my-site-
			 */
			$thirdParties['activecampaign'] = array(
					'label' 	=> __( 'Active Campaign', 'wpca' ),
					'js' => array(
						// FULL URL: cbowe.activehosted.com/f/embed.php?id=xyz
						'.activehosted.com/f/embed',
						// WordPress plugin: https://wordpress.org/plugins/activecampaign-subscription-forms/
						'wp-content/plugins/activecampaign-subscription-forms/site_tracking.js'
					),
					'js_needle' => 'trackcmp.net/visit?actid=',
					'cc'	=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['wpstats'] = array(
					'label' => __( 'WP Stats', 'wpca' ),
					'js' 	=> 'stats.wp.com',
					'cc'	=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['twitterads'] = array(
					'label' => __( 'Twitter ads', 'wpca' ),
					'js' 	=> 'static.ads-twitter.com',
					'js_needle' => array(
							'.twq',
							'static.ads-twitter.com',
					),
					'cc'	=> self::CC_IDX_ADVERTISING
			);

			$thirdParties['linkedininsight'] = array(
					'label' 	=> __( 'LinkedIn Insight', 'wpca' ),
					'js_needle' => 'snap.licdn.com',
					'cc'	=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['cxense'] = array(
					'label' 	=> __( 'Cxense', 'wpca' ),
					'js' 		=> 'cdn.cxense.com',
					'js_needle' => 'cdn.cxense.com',
					'cc'	=> self::CC_IDX_OTHER

			);

			$thirdParties['pardot'] = array(
					'label' 	=> __( 'Pardot', 'wpca' ),
					'js_needle' => 'pardot.com/pd.js',
					'cc'		=> self::CC_IDX_OTHER
			);

			$thirdParties['zopim'] = array(
					'label'     => __( 'Zendesk chat', 'wpca' ),
					'js'        => 'static.zdassets.com/ekr/snippet.js',
					'js_needle' => array(
							'zopim.com',
							'$zopim',
							'zESettings.webWidget',
							'zE(\'webWidget',
							'window.zEmbed',
							'window.zE',
							'assets.zendesk.com'
					),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['sumo'] = array(
					'label' 	=> __( 'Sumo', 'wpca' ),
					'js' => array(
						'load.sumo.com',
						'load.sumome.com'
					),
					'js_needle' => 'sumoSiteId',
					'cc'		=> self::CC_IDX_OTHER
			);

			$thirdParties['onesignal'] = array(
					'label'		=> __( 'OneSignal', 'wpca' ),
					'js'       	=> 'cdn.onesignal.com/sdks/OneSignalSDK.js',
					'js_needle' => 'window.OneSignal',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['likebtn'] = array(
					'label'		=> __( 'LikeBtn', 'wpca' ),
					'js'		=> array(
						'w.likebtn.com', // full URL: //w.likebtn.com/js/w/widget.js
						'wp-content/plugins/likebtn-like-button'
					),
					'js_needle'	=> 'w.likebtn.com',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['tawkto'] = array(
					'label' 	=> __( 'Tawk.to', 'wpca' ),
					'js_needle' => array(
							'Tawk_API',
							'embed.tawk.to'
					),
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);
			$thirdParties['medianet'] = array(
					'label'     => __( 'Media.net ', 'wpca' ),
					'js_needle' => 'contextual.media.net',
					'js' => 'contextual.media.net', // full URL: https://contextual.media.net/nmedianet.js, https://contextual.media.net/fcmdynet.js
					'cc'		=> self::CC_IDX_ADVERTISING
			);

			$thirdParties['hotjar'] = array(
					'label'		=> __( 'Hotjar', 'wpca' ),
					'js_needle' => array(
							'h._hjSettings',
							'static.hotjar.com'
					),
					'cc'		=> self::CC_IDX_ANALYTICAL
			);

			$thirdParties['hubspot'] = array(
					'label' 	=> __( 'HubSpot', 'wpca' ),
					's'			=> 'app.hubspot.com', // https://app.hubspot.com/meetings/your/calendarurl?
					'js'		=> array(
							'js.hsforms.net/forms', // full URL: //js.hsforms.net/forms/v2.js
							'js.hs-scripts.com', // full URL: //js.hs-scripts.com/123456.js
							// Call to Action (CTA) script
							'js.hscta.net', // js.hscta.net/cta/current.js
							// Meetings Embed Script
							'static.hsappstatic.net' // static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js
					),
					'js_needle' => array( 'hbspt', '_hsq' ),
					'html_elem' => array(
							// Call to Action HTML wrapper div
							// @see https://knowledge.hubspot.com/articles/kcs_article/ctas/embed-calls-to-action-cta-on-an-external-site
							array(
								'name' => 'span',
								'attr' => 'class:hs-cta-node'
							),
							// Call to Action image
							array(
									'name' => 'img',
									'attr' => 'src:no-cache.hubspot.com'
							),
							// Meetings Embed container div
							// @see https://knowledge.hubspot.com/articles/kcs_article/meetings/how-can-i-embed-a-meeting-on-my-website-with-hubspot-sales
							array(
								'name' => 'div',
								'attr' => 'class:meetings-iframe-container'
							)
					),
					'cc'		=> self::CC_IDX_OTHER
			);

			$thirdParties['tripadvisor'] = array(
					'label'     => __( 'Tripadvisor', 'wpca' ),
					'js'        => 'jscache.com/wejs', // full URL: https://www.jscache.com/wejs
					'js_needle' => 'jscache.com/wejs',
					'html_elem' => array(
						'name' => 'img',
						'attr' => 'src:tripadvisor'
					),
					'cc'        => self::CC_IDX_OTHER
			);

			// https://statcounter.com/support/knowledge-base/14/
			$thirdParties['statcounter'] = array(
					'label'     => __( 'StatCounter', 'wpca' ),
					'js'        => 'secure.statcounter.com',
					'js_needle' => array(
							'sc_project',
							'secure.statcounter.com'
					),
					'html_elem' => array(
						'name' => 'div',
						'attr' => 'class:statcounter'
					),
					'cc'        => self::CC_IDX_ANALYTICAL
			);

			$thirdParties['drift'] = array(
					'label'		=> __( 'Drift', 'wpca' ),
					'js'		=> 'js.driftt.com',
					'js_needle'	=> 'window.driftt',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['spotify'] = array(
					'label'		=> __( 'Spotify', 'wpca' ),
					's' 		=> '.spotify.com',
					'cc'		=> self::CC_IDX_SOCIAL_MEDIA
			);

			$thirdParties['paypal_ms'] = array(
				'label' => __( 'PayPal Marketing Solutions', 'wpca' ),
				'js' => array(
					 'www.paypalobjects.com',
					 'paypal.com/tagmanager'
				),
				'js_needle' => array(
					'MUSEButton',
					'paypalDDL',
					'paypal.com/tagmanager'
				),
				'html_elem' => array(
					'name' => 'div',
					'attr' => 'id:paypal-marketing-button-container'
				),
				'cc' => self::CC_IDX_ADVERTISING
			);

			$thirdParties['pys'] = array(
				'label' => __( 'Pixel your site', 'wpca' ),
				'js' => [
					[
						's' => 'wp-content/plugins/pixelyoursite/dist/scripts/public.js',
						'dep' => 'pysOptions',
						'dt' => 200
					],
					[
						's' => 'wp-content/plugins/pixelyoursite/dist/scripts/public.js',
						'dep' => 'pysOptions',
						'dt' => 200
					]
				],
				'js_needle' => [
					'pysOptions'
				],
				'cc' => self::CC_IDX_ANALYTICAL
			);
			
			// https://docs.microsoft.com/en-us/clarity/clarity-setup
			$thirdParties['microsoft_clarity'] = array(
				'label' => __( 'Microsoft Clarity', 'wpca' ),
				'js_needle' => [ 'clarity.ms/tag' ],
				'cc' => self::CC_IDX_ANALYTICAL
			);

			$thirdParties['whatsapp'] = array(
				'label' => __( 'WhatsApp', 'wpca' ),
				'js_needle' => [
					'api.whatsapp.com', 'web.whatsapp.com'
				],
				'js' => [
					'api.whatsapp.com', 'web.whatsapp.com'
				],            
				// https://codecanyon.net/item/wordpress-whatsapp-support/20963962
				'html_elem' => [
					'elem' => 'div',
					'attr' => 'class:wws-popup-container'
				],
				'cc' => self::CC_IDX_OTHER
			);

			$thirdParties['asl'] = array(
				'label' => __( 'Agile Store Locator', 'wpca' ),
				'html_elem' => [
					'elem' => 'div',
					'attr' => 'class:storelocator-main'
				],
				'cc' => self::CC_IDX_OTHER
			);

			/**
			 * Allow others to add items to the thirdparty array
			 *
			 * @param	array	$thirdParties
			 *
			 * @since 	2.2.7
			 *
			 * @deprecated 3.2.11 thirparties has a typo and should be thirdparties
			 */
			$thirdParties = apply_filters( 'wpca_automate_thirparties' , $thirdParties );

			/**
			 * Allow others to add items to the thirdparty array
			 *
			 * @param	array	$thirdParties
			 *
			 * @since 	3.2.11
			 */
			$thirdParties = apply_filters( 'wpca_automate_thirdparties' , $thirdParties );

			foreach ( $thirdParties as $key => $data ) {
				if( !is_string( $key ) ) {
					throw new WpieRunTimeException( sprintf( __( "Invalid index found in the thirdparties array. Index should be of type 'string'. Index found: %d.", 'wpca' ), $key ) );
					break;
				}

				// init params for the current cycle
				$s = $label = $js = $jsNeedle = $cb = $htmlElems = null;
				$hasJs = $hasJsNeedle = false;
				// the default callback
				$defaultCallback = '_automate' . ucfirst( $key );

				// do not use '&$this' as this leads to error:
				// Deprecated: Call-time pass-by-reference has been deprecated
				$defaultCallbackExist = method_exists( $this , $defaultCallback );

				$thirdParties[$key]['has_s'] = false;
				$thirdParties[$key]['has_js'] = false;
				$thirdParties[$key]['has_js_needle'] = false;
				$thirdParties[$key]['has_cc'] = false;
				$thirdParties[$key]['has_html_elem'] = false;
				$thirdParties[$key]['internal_cb'] = false;

				if( !isset( $data['label'] ) ) {
					$label = ucfirst( $key );
					$thirdParties[$key]['label'] = $label;
				} elseif ( is_string( $data['label'] ) ) {
					$label = sanitize_text_field( $data['label'] );
					$thirdParties[$key]['label'] =  $label;
				}

				if( !isset( $data['s'] ) ) {
					$thirdParties[$key]['s'] = $s;
				} elseif ( is_string( $data['s'] ) ) {
					$s = sanitize_text_field( $data['s'] );
					$thirdParties[$key]['s'] =  $s;
					$thirdParties[$key]['has_s'] = true;
				} elseif ( is_array( $data['s'] ) ) {
					foreach ( $data['s'] as $k => $v ) {
						if( is_string( $v ) ) {
							$thirdParties[$key]['s'][$k] = sanitize_text_field( $v );
							$has_s = true;
						} else {
							$thirdParties[$key]['s'] = $s;
							$has_s = false;
							break;
						}
					}
					$thirdParties[$key]['has_s'] = $has_s;
				}

				// @todo check if sanitize_text_field does not breaks js strings
				if( !isset( $data['js'] ) ) {
					$thirdParties[$key]['js'] = $js;
				} elseif ( is_string( $data['js'] ) ) {
					$thirdParties[$key]['js'] = [];
					$thirdParties[$key]['js'][] = [
						's' => sanitize_text_field( $data['js'] ),
						'dep' => '',
						'dt' => 0,
						'r' => 3
					];
					$thirdParties[$key]['has_js'] = true;
				} elseif ( is_array( $data['js'] ) ) {
					foreach ( $data['js'] as $k => $v ) {
						if( is_string( $v ) ) {
							$thirdParties[$key]['js'][$k] = [
								's' => sanitize_text_field( $v ),
								'dep' => '',
								'dt' => 0,
								'r' => 3
							];
							$hasJs = true;
						} elseif( is_array( $v ) ) {
							// new style with dependencies
							$s = null;
							$dep = '';
							$dt = 0;
							$retries = 3;

							if( isset( $v['s'] ) && is_string( $v['s'] ) ) {
								$s = sanitize_text_field( $v['s'] );
								$hasJs = true;
							}
							if( isset( $v['dep'] ) && is_string( $v['dep'] ) ) {
								$dep = sanitize_text_field( $v['dep'] );
							}
							if( isset( $v['dt'] ) && is_numeric( $v['dt'] ) ) {
								$dt = ceil( (int)$v['dt'] );
							}
							if( isset( $v['r'] ) && is_numeric( $v['r'] ) ) {
								$retries = ceil( (int)$v['r'] );
							}

							$thirdParties[$key]['js'][$k]['s'] = $s;
							$thirdParties[$key]['js'][$k]['dep'] = $dep;
							$thirdParties[$key]['js'][$k]['dt'] = $dt;
							$thirdParties[$key]['js'][$k]['r'] = $retries;
						} else {
							$thirdParties[$key]['js'] = $js;
							$hasJs = false;
							break;
						}
					}
					$thirdParties[$key]['has_js'] = $hasJs;
				}

				if( !isset( $data['js_needle'] ) ) {
					$thirdParties[$key]['js_needle'] = $jsNeedle;
				} elseif ( is_string( $data['js_needle'] ) ) {
					// old style without dependencies
					$thirdParties[$key]['js_needle'] = [];
					$thirdParties[$key]['js_needle'][] = [
						's' => sanitize_text_field( $data['js_needle'] ),
						'dep' => '',
						'dt' => 0,
						'r' => 3
					];
					$thirdParties[$key]['has_js_needle'] = true;
				} elseif ( is_array( $data['js_needle'] ) ) {
					foreach ( $data['js_needle'] as $k => $v ) {
						// old style without dependencies
						if( is_string( $v ) ) {
							$thirdParties[$key]['js_needle'][$k] = [
								's' => sanitize_text_field( $v ),
								'dep' => '',
								'dt' => 0,
								'r' => 3
							];
							$hasJsNeedle = true;
						} elseif( is_array( $v ) ) {
							// new style with dependencies
							$s = null;
							$dep = '';
							$dt = 0;
							$retries = 3;

							if( isset( $v['s'] ) && is_string( $v['s'] ) ) {
								$s = sanitize_text_field( $v['s'] );
								$hasJsNeedle = true;
							}
							if( isset( $v['dep'] ) && is_string( $v['dep'] ) ) {
								$dep = sanitize_text_field( $v['dep'] );
							}
							if( isset( $v['dt'] ) && is_numeric( $v['dt'] ) ) {
								$dt = ceil( (int)$v['dt'] );
							}
							if( isset( $v['r'] ) && is_numeric( $v['r'] ) ) {
								$retries = ceil( (int)$v['r'] );
							}

							$thirdParties[$key]['js_needle'][$k]['s'] = $s;
							$thirdParties[$key]['js_needle'][$k]['dep'] = $dep;
							$thirdParties[$key]['js_needle'][$k]['dt'] = $dt;
							$thirdParties[$key]['js_needle'][$k]['r'] = $retries;
						} else {
							$thirdParties[$key]['js_needle'] = $jsNeedle;
							$hasJsNeedle = false;
							break;
						}
					}
					$thirdParties[$key]['has_js_needle'] = $hasJsNeedle;
				}

				if ( isset( $data['callback'] ) && is_string( $data['callback'] ) && !empty( $data['callback'] ) ) {
					$cb = trim( $data['callback'] );
				} elseif ( isset( $data['callback'] ) && is_array( $data['callback'] ) && 2 === count( $data['callback'] ) ) {
					$cbMethod = trim( $data['callback'][1] );
					$data['callback'][1] = $cbMethod;
					$cb =& $data['callback'];
				} elseif( !isset( $data['callback'] ) && $defaultCallbackExist ) {
					$cb = $defaultCallback;
				} else {
					$cb = self::DEFAULT_AUTOMATE_CB_STR;
				}

				// cookie categories
				if( !isset( $data['cc'] ) ) {
					$thirdParties[$key]['cc'] = self::CC_IDX_DEFAULT;
				} elseif( is_string( $data['cc'] ) ) {
					$thirdParties[$key]['has_cc'] = true;
					$cc = sanitize_title( $data['cc'] );
					if( !$this->isAllowedCookieCategory( $cc ) ) {
						$thirdParties[$key]['cc'] = self::CC_IDX_DEFAULT;
					} else {
						$thirdParties[$key]['cc'] = $cc;
					}
				}

				// HTML elements
				if( isset( $data['html_elem'] ) && is_array( $data['html_elem'] ) ) {
					$hasHtmlElem = true;
					// force single items to be in an array
					if( count( $data['html_elem'] ) && !is_numeric( array_keys( $data['html_elem'] )[0] ) ) {
						$data['html_elem'] = array( $data['html_elem'] );
						$thirdParties[$key]['html_elem'] = $data['html_elem'];
					}
					// loop over HTML elements
					foreach ( $data['html_elem'] as $k => $htmlElem ) {
						$regexPrefix = $regexSuffix = '';

						if( !isset( $htmlElem['name'] ) ) {
							$hasHtmlElem = false;
						} elseif( isset( $htmlElem['name'] ) && !is_string( $htmlElem['name'] ) ) {
							$hasHtmlElem = false;
						}
						if( !isset( $htmlElem['attr'] ) ) {
							$hasHtmlElem = false;
						} elseif( isset( $htmlElem['attr'] ) && !is_string( $htmlElem['attr'] )) {
							$hasHtmlElem = false;
						} elseif( isset( $htmlElem['attr'] ) ) {
							$pos = strpos( $htmlElem['attr'], ':' );
							if( false === $pos || $pos < 1 ) {
								$hasHtmlElem = false;
							}
							if( $hasHtmlElem && isset( $htmlElem['placeholder'] ) && is_bool( $htmlElem['placeholder'] ) ) {
								$thirdParties[$key]['html_elem'][$k]['fp'] = true;
								$thirdParties[$key]['html_elem'][$k]['placeholder'] = $htmlElem['placeholder'];
							} else {
								$thirdParties[$key]['html_elem'][$k]['fp'] = false;
							}
						}
						// handle regex prefix and suffix patterns
						if( isset( $htmlElem['regex_prefix'] ) ) {
							$regexPrefix = sanitize_text_field( $htmlElem['regex_prefix'] );
						}
						if( isset( $htmlElem['regex_suffix'] ) ) {
							$regexSuffix = sanitize_text_field( $htmlElem['regex_suffix'] );
						}

						if( !$hasHtmlElem ) {
							unset( $thirdParties[$key]['html_elem'][$k] );
							continue;
						}

						// everything fine now, process html elem
						$thirdParties[$key]['html_elem'][$k]['name'] = sanitize_key( $htmlElem['name'] );
						$attr = trim( $htmlElem['attr'] );
						$attrArr = explode( ':', $attr );

						$thirdParties[$key]['html_elem'][$k]['attr'] = sprintf(
							'%s:%s',
							sanitize_key( $attrArr[0] ),
							sanitize_text_field( $attrArr[1] )
						 );
						$thirdParties[$key]['html_elem'][$k]['regex_prefix'] = $regexPrefix;
						$thirdParties[$key]['html_elem'][$k]['regex_suffix'] = $regexSuffix;
						$thirdParties[$key]['has_html_elem'] = $hasHtmlElem;
					}
				} else {
					$thirdParties[$key]['html_elem'] = $htmlElems;
				}

				// for internal callback
				if( method_exists( $this, $cb ) ) {
					$thirdParties[$key]['internal_cb'] = true;
				}
				$thirdParties[$key]['callback'] = $cb;
			}

			return $thirdParties;
		}


		/**
		 * Get Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function getCookieCategories()
		{
			return $this->_cookieCategories;
		}


		/**
		 * Get allowed Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function getAllowedCookieCategories()
		{
			return $this->_allowedCookieCategories;
		}


		/**
		 * Get required Cookie Categories
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function getRequiredCookieCategories()
		{
			return $this->_requiredCookieCategories;
		}


		/**
		 * Get the Cookie Categories for the current client
		 *
		 * @uses WpieCookieHelper::read()
		 * @uses WpieMiscHelper::getStringParts()
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function getClientCookieCategories()
		{
			static $categories = null;

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

			$categories = $this->_getClientCookieCategoriesFromCookie();

			return $categories;
		}

		/**
		 * Get the 'checked' Cookie Categories
		 *
		 * The entries in the automate data array which are marked as 'checked'
		 *
		 * @uses self::getChecked()
		 * @uses self::getCookieCategories()
		 *
		 * @since 3.2.3
		 *
		 * @return array
		 */
		public function getCheckedCookieCategories()
		{
			static $categories = null;

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

			$checkedCategories = array();

			// get checked categories
			$checked = $this->getChecked();
			$categories = $this->getCookieCategories();

			foreach( $checked as $item ) {
				if( isset( $item['cc'] ) ) {
					$checkedCategories[] = $item['cc'];
				}
			}
			// remove duplicates
			$checkedCategories = array_unique( $checkedCategories );
			$keys = array_flip( $checkedCategories );

			foreach ( $categories as $k => $v ) {
				if( !isset( $keys[$k] ) && !$this->isRequiredCookieCategory( $k ) ) {
					unset( $categories[$k] );
				}
			}

			unset( $checkedCategories, $checked, $keys );

			return $categories;
		}


		/**
		 * Get all checked third parties
		 *
		 * @access public
		 *
		 * @uses self::_getAutomateData()
		 *
		 * @since 2.3
		 *
		 * @return array
		 */
		public function getChecked()
		{
			$checked = array();
			$data = $this->_getAutomateData();

			foreach ( $data as $k => $v ) {
				if( isset( $v['check'] ) && $v['check'] ) {
					$checked[$k] = $v;
				}
			}

			return $checked;
		}


		/**
		 * Determine if having Cookie Categories for the current client
		 *
		 * @since 3.2
		 *
		 * @return bool
		 */
		public function hasClientCookieCategories()
		{
			return $this->_hasClientCookieCategories;
		}


		/**
		 * Determine if client has passed category set
		 *
		 * @param array $category
		 *
		 * @since 3.2
		 *
		 * @return boolean
		 */
		public function hasClientThisCookieCategory( $category = '' )
		{
			return ( $this->hasClientCookieCategories() && in_array( $category, $this->_clientCookieCategories ) );
		}


		/**
		 * @return array
		 */
		public function getClientCookieCategoriesDeclined()
		{
			$client = $this->getClientCookieCategories();
			$allowed = $this->getAllowedCookieCategories();

			$declined = array_diff( $allowed, $client );

			return $declined;
		}


		/**
		 * @return boolean
		 */
		public function hasClientCookieCategoriesAll()
		{
			if( $this->hasClientCookieCategories() ) {
				$declined = $this->getClientCookieCategoriesDeclined();

				return ( 0 === count( $declined ) );
			} else {
				return false;
			}
		}

		/**
		 * Detremine if given category is an allowed Cookie Category
		 *
		 * @param string $category
		 *
		 * @since 3.2
		 *
		 * @return bool
		 */
		public function isAllowedCookieCategory( $category = '' )
		{
			$allowed = $this->getAllowedCookieCategories();

			return ( in_array( $category, $allowed ) );
		}

		/**
		 * Detremine if given category is a required Cookie Category
		 *
		 * @param string $category
		 *
		 * @since 3.2.3
		 *
		 * @return bool
		 */
		public function isRequiredCookieCategory( $category = '' )
		{
			$required = $this->getRequiredCookieCategories();

			return ( in_array( $category, $required ) );
		}


		/**
		 * Callback for the wpca_registered_shortcodes hook
		 *
		 * Register the shortcodes for this module
		 *
		 * @param array $registered
		 *
		 * @since 3.1.2
		 *
		 * @return array
		 */
		public function registerShortcodes( $registered = array() )
		{
			$shordcodes = array(
				'wpca_cookie_allow_code' => 'doShortcodeCookieAllowCode',
				'wpca_cc_settings_box' => 'doShortcodeCookieCategorySettingsBox',
				'wpca_cc_settings_options' => 'doShortcodeCookieCategorySettingsOptions',
				'wpca_cc_settings_btn_accept_all' => 'doShortcodeCookieCategorySettingsBtnAcceptAll',
				'wpca_cc_settings_btn_save' => 'doShortcodeCookieCategorySettingsBtnSave',
			);

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

				if( !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_allow_code hook
		 *
		 * @access public
		 *
		 * @param string $content
		 * @param array $atts
		 * @param string $tag
		 *
		 * @uses shortcode_atts()
		 * @uses WpcaCore::doBlockCookies()
		 * @uses htmlspecialchars_decode()
		 * @uses do_shortcode()
		 *
		 * @since 3.1.2
		 * @since 3.2 checking on passed cookie category
		 *
		 * @return string if cookie allow code validates to true, else void
		 */
		public function doShortcodeCookieAllowCode( $atts, $content = null, $tag = '' )
		{
			if( null !== $content ) {
				$atts = shortcode_atts(
						array(
								'txt' => '',
								'type' => '',
								'cc' => self::CC_IDX_DEFAULT
						), $atts, $tag );

				$txt = $atts['txt'];
				$type = $atts['type'];
				$cc = trim( $atts['cc'] );

				$patterns = array();
				if( false !== strpos( $content, '<script' ) ) {
					$patterns[] = sprintf(  "#$this->_regexPatternScriptSrc#", '', '' );
				}
				if( false !== strpos( $content, '<iframe' ) ) {
					$patterns[] = sprintf( "#$this->_regexPatternIframe#", '', '' );
				}
				if( false !== strpos( $content, '<img' ) ) {
					$patterns[] = sprintf( "#$this->_regexPatternHtmlElemWithAttr#", 'img', 'src', '', '', '', 'img', 'img' );
				}
				if( false !== strpos( $content, '<a' ) ) {
					$patterns[] = sprintf( "#$this->_regexPatternHtmlElemWithAttr#", 'a', 'href', '', '', '', 'a', 'a' );
				}

				if( count( $patterns ) ) {
					$_content = $content;
					$this->_currentData = [
						'type' => $type,
						'cc' => $cc
					];
					$this->_currentType = $type;
					$this->_currentCc = $cc;
					$this->_currentTxt = $txt;

					$content = preg_replace_callback( $patterns, array( $this, '_eraseReplaceCbBody' ), $content );
					// check for preg_replace errors
					if( null === $content ) {
						$content = $_content;
						if( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
							error_log( 'WeePie: An error occured calling preg_replace_callback() inside shortcode [wpca_cookie_allow_code]');
						}
					}

					$this->_currentType = null;
					$this->_currentCc = null;
					$this->_currentTxt = '';
					unset( $_content );
				} else {
					$content = "<wpca-block>$content</wpca-block>";

					/**
					 * Let others filter the blocked content inside the shortcode
					 *
					 * @param string $txt
					 * @param string $type
					 * @param string $content
					 * @param string $content
					 * @param string $cc
					 * @param array  $matches
					 *
					 * @since 3.2 added the $cc param
					 *
					 * See for more details sef::_eraseReplaceCbHead()
					 */
					$content = apply_filters( 'wpca_replacement_text', $txt, $type, 'body', $content, $cc, array( 'EL_START'=>'<wpca-block' ) );
				}

				return htmlspecialchars_decode( do_shortcode( $content ) );
			}
		}


		/**
		 * Callback for the wpca_shortcode_wpca_cc_settings_box hook
		 *
		 * @access public
		 *
		 * @param string $content
		 * @param array $atts
		 * @param string $tag
		 *
		 * @uses shortcode_atts()
		 * @uses do_shortcode()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function doShortcodeCookieCategorySettingsBox( $atts, $content = null, $tag = '' )
		{
			$atts = shortcode_atts(
					array(
							// no attr yet
					), $atts, $tag );

			// $foo = $atts['foo']

			return $this->renderCookieCategorySettingsBox();
		}


		/**
		 * Callback for the wpca_shortcode_wpca_cc_settings_options hook
		 *
		 * @access public
		 *
		 * @param string $content
		 * @param array $atts
		 * @param string $tag
		 *
		 * @uses shortcode_atts()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function doShortcodeCookieCategorySettingsOptions( $atts, $content = null, $tag = '' )
		{
			$atts = shortcode_atts(
					array(
							// no attr yet
					), $atts, $tag );

			// $foo = $atts['foo']

			return $this->renderCookieCategorySettingsOptions();
		}


		/**
		 * Callback for the wpca_shortcode_wpca_cc_settings_btn_accept_all hook
		 *
		 * @access public
		 *
		 * @param string $content
		 * @param array $atts
		 * @param string $tag
		 *
		 * @uses shortcode_atts()
		 * @uses WpcaModuleFrontend::renderBtnAccept()
		 *
		 * @since 3.2.13
		 *
		 * @return string
		 */
		public function doShortcodeCookieCategorySettingsBtnAcceptAll( $atts, $content = null, $tag = '' )
		{
			/** @var WpcaModuleFrontend $frontend */
			$frontend = $this->getModule( 'frontend' );

			$atts = shortcode_atts(
				array(
						'txt' => '',
						'txt_accepting' => '',
				), $atts, $tag );

			$txt = $atts['txt'];
			$txtAccepting = $atts['txt_accepting'];

			if( '' === $txt ) {
				$txt = $this->settingsProcessor->get('content')->get( 'content_cc_button_accept_all_txt' );
			}
			if( '' === $txtAccepting ) {
				$txtAccepting = $this->settingsProcessor->get('content')->get( 'content_cc_button_accept_all_txt_processing' );
			}

			return $frontend->renderBtnAccept( $txt, $txtAccepting );
		}


		/**
		 * Callback for the wpca_shortcode_wpca_cc_settings_btn_save hook
		 *
		 * @access public
		 *
		 * @param string $content
		 * @param array $atts
		 * @param string $tag
		 *
		 * @uses shortcode_atts()
		 *
		 * @since 3.2
		 *
		 * @return string
		 */
		public function doShortcodeCookieCategorySettingsBtnSave( $atts, $content = null, $tag = '' )
		{
			$atts = shortcode_atts(
					array(
							// no attr yet
					), $atts, $tag );

			// $foo = $atts['foo']

			return $this->renderCookieCategorySettingsBtnSave();
		}


		/**
		 * Callback for the wpca_validated_data_wpca_settings_general hook
		 *
		 * add data to the array
		 *
		 * @acces 	public
		 *
		 * @param 	array 	$data
		 * @param	bool	$hasWarnings
		 *
		 * @since 	2.2.7
		 *
		 * @return 	array	the filtered array
		 */
		public function validated( $data, $hasWarnings = false )
		{
			if( isset( $data['automate_data'] ) ) {
				$this->_hasChecked = false;
				$automateData = $data['automate_data'];
				$thirdpartyData = $this->getThirdParties();

				// validate the check all box
				if( !isset( $data['automate_check_all'] ) )
					$data['automate_check_all'] = 0;
				else
					$data['automate_check_all'] = (int)$data['automate_check_all'];

				foreach( $automateData as $key => $autodata ) {

					if( !isset( $thirdpartyData[$key] ) ) {
						unset( $data['automate_data'][$key] );
						continue;
					}

					if( !isset( $autodata['check'] ) )
						$data['automate_data'][$key]['check'] = 0;
					else {
						$this->_hasChecked = true;
						$data['automate_data'][$key]['check'] = (int)$autodata['check'];
					}

					// the data integrity should be handled already inside WpcaAutomate::getThirdParties()
					// therefor the values could be added directly to the $dara array
					$data['automate_data'][$key]['label'] 	 		= $thirdpartyData[$key]['label'];
					$data['automate_data'][$key]['callback'] 		= $thirdpartyData[$key]['callback'];
					$data['automate_data'][$key]['s'] 	     		= $thirdpartyData[$key]['s'];
					$data['automate_data'][$key]['js'] 	     		= $thirdpartyData[$key]['js'];
					$data['automate_data'][$key]['js_needle'] 		= $thirdpartyData[$key]['js_needle'];
					$data['automate_data'][$key]['has_s'] 	    	= $thirdpartyData[$key]['has_s'];
					$data['automate_data'][$key]['has_js'] 	    	= $thirdpartyData[$key]['has_js'];
					$data['automate_data'][$key]['has_js_needle'] 	= $thirdpartyData[$key]['has_js_needle'];
					$data['automate_data'][$key]['internal_cb'] 	= $thirdpartyData[$key]['internal_cb'];
					$data['automate_data'][$key]['cc'] 				= $thirdpartyData[$key]['cc'];
					$data['automate_data'][$key]['has_cc'] 			= $thirdpartyData[$key]['has_cc'];
					$data['automate_data'][$key]['has_html_elem'] 	= $thirdpartyData[$key]['has_html_elem'];
					$data['automate_data'][$key]['html_elem'] 		= $thirdpartyData[$key]['html_elem'];
				}

				unset( $automateData, $thirdpartyData );
			}

			return $data;
		}


		/**
		 * Callback for the wpca_settings_skip_fields hook
		 *
		 * Skip the "automate_data" array when satinizing
		 * Skip checkboxes also
		 *
		 * @acces 	public
		 *
		 * @param 	array 	$skip
		 * @param 	string 	$setting
		 *
		 * @since	2.2.7
		 *
		 * @return 	array 	the filtered array
		 */
		public function skipSatinizing( $skip, $setting )
		{
			if( $this->settingsProcessor->getName( 'general' ) !== $setting ) {
				return $skip;
			}

			$skip[] = 'automate_data';
			$skip[] = 'general_automate_enable_auto_erase';
			$skip[] = 'general_automate_enable_ga_anonymized';

			return $skip;
		}


		/**
		 * Callback for the wpca_update_settings hook
		 *
		 * Filter the 'updates' array. Main tasks are:
		 *  - update the 'automate_data' setting form fields
		 *  - update the 'cc_data' settings form fields
		 *
		 * @access	public
		 *
		 * @param 	array 	$updates
		 * @param 	array 	$data
		 * @param 	string	$setting the current settings DB name
		 *
		 * @since	2.2.7
		 * @since   3.2.11 added logic for the Cookie Category data (cc_data)
		 *
		 * @return	array 	with updated setting form fields
		 */
		public function updateSetting( $updates, $data, $setting )
		{
		    if( $this->settingsProcessor->getName( 'general' ) !== $setting &&
		        $this->settingsProcessor->getName( 'content' ) !== $setting ) {
				return $updates;
			}

			// Update the 3rd party table values
			if( $this->settingsProcessor->getName( 'general' ) === $setting ) {
			    if( isset( $data['automate_data'] ) ) {
			        $updates['automate_data'] = $data['automate_data'];
			    }

			    $updates['automate_check_all'] = ( isset( $data['automate_check_all'] ) && $data['automate_check_all'] ) ? true : false;
			    $updates['general_automate_enable_auto_erase'] = ( isset( $data['general_automate_enable_auto_erase'] ) ) ? true : false;
			    $updates['general_automate_enable_ga_anonymized'] = ( isset( $data['general_automate_enable_ga_anonymized'] ) ) ? true : false;

			}

			// Sanitize and update the Cookie Category data values
			if( $this->settingsProcessor->getName( 'content' ) === $setting ) {
			    if( isset( $data['cc_data'] ) ) {
			        foreach( $data['cc_data'] as $idx => $ccData ) {
			            if( isset( $ccData['label'] ) ) {
			                $updates['cc_data'][$idx]['label'] = sanitize_text_field( $ccData['label'] );
			            }
			            if( isset( $ccData['descr'] ) ) {
			                $updates['cc_data'][$idx]['descr'] = sanitize_textarea_field( $ccData['descr'] );
			            }
			        }
			    }
			}

			return $updates;
		}


		/**
		 * Callback for the wpca_activate_plugin hook
		 *
		 * Unset the automate_data entry in the wpca_settings_general wp_options table
		 *
		 * @access	public
		 *
		 * @param	bool $upgrading flag is plugin is being updated to a newer version
		 *
		 * @uses 	WpcaAutomate::_flush()
		 *
		 * @since 	2.2.7
		 */
		public function activatePlugin( $upgrading )
		{
			$this->_flush();
		}


		/**
		 * Skip the replacement of Google Analytics if IP's are _anonymized
		 *
		 * Return early if not in $context 'head' or $type is not 'googleanalytics' or 'google_gtag_*'
		 *
		 * Search for one of these:
		 * 	- _gat._anonymizeIp
		 *  - ga('set', 'anonymizeIp', true)
		 *  - gtag('config', { 'anonymize_ip': true });
		 *
		 * @param bool $skip
		 * @param string $type
		 * @param string $context
		 * @param string $match
		 *
		 * @since 2.3
		 * @since 3.2.13 added support for gtag
		 *
		 * @return bool
		 */
		public function skipAnonymizedGa( $skip, $type, $context, $match )
		{
			$pattern = '';
			$isGa = self::AR_KEY_GA === $type;
			$isGtag = false !== strpos( $type, WpcaModuleAutomate::AR_KEY_GTAG_PREFIX );

			if( 'head' !== $context ) {
				return $skip;
			} elseif( !$isGa && !$isGtag ) {
				return $skip;
			} elseif( $isGa ) {
				$pattern =
				'(?://)?'.
				'\s*?'.
				'('.
					'_gat\._anonymizeIp'.
					'|'.
					'ga\((?P<Q>"|\')set(?P=Q),\s*?(?P<Q2>"|\')anonymizeIp(?P=Q2),\s*?true\)'.
					'|'.
					'__gaTracker\((?P<Q3>"|\')set(?P=Q3),\s*?(?P<Q4>"|\')anonymizeIp(?P=Q4),\s*?true\)'.
					')';
			} elseif( $isGtag ) {
				$pattern =
				'(?://)?'.
				'\s*?'.
				'gtag\('.
					'(?P<Q>"|\')'.
						'config'.
					'(?P=Q)'.
					',\s*'.
					'(?P<Q2>\"|\')'.
					'[^\'"]+'.
					'(?P=Q2)'.
					',\s*'.
					'\{'.
						'\s*'.
						'(?P<Q3>"|\')'.
							'anonymize_ip'.
						'(?P=Q3)'.
						'\:'.
							'\s*true'.
						'[^}]+'.
					'\}\s*'.
				'\)\s*'.
				';?';
			}

			if( '' !== $pattern && preg_match( "#$pattern#", $match, $m ) ) {
					$anonymizeStr = $m[1];
					return ( false === strpos( $anonymizeStr, '//' ) );
			}

			return $skip;
		}


		/**
		 * Callback for the template_include hook
		 *
		 * This is the very last point before the template gets included.
		 * Start output buffering now
		 *
		 * @uses ob_start()
		 * @uses is_robots()
		 * @uses is_feed()
		 * @uses is_robots()
		 *
		 * @since 2.3
		 */
		public function bufferStart()
		{
			// Stop here for robots.txt, feed or trackback requests
			if( is_robots() ) {
				return;
			} elseif ( is_feed() ) {
				return;
			} elseif( is_trackback() ) {
				return;
			}

			ob_start( array( $this, 'bufferEnd' ) );
		}


		/**
		 * Callback for the template_include hook
		 *
		 * This is the very last point before the the buffer is send to the browser.
		 * End output bufferig now and apply the automate logic to the output buffer (the whole HTML page)
		 *
		 * @param string buffer
		 *
		 * @uses 	WpcaAutomate::_getAutomateData()
		 * @uses	WpcaAutomate::_doAutomate()
		 *
		 * @since	2.3
		 * @since   3.2.5 this is the ob_start callback
		 */
		public function bufferEnd( $buffer = '' )
		{
			try {
				// return the buffer directly if buffer is empty
				if( '' === trim( $buffer ) ) {
					return $buffer;
				}

				/**
				 * Filter the buffer before processing
				 *
				 * @var string $buffer
				 *
				 * @since 3.2.3
				 */
				$buffer = apply_filters( 'wpca_automate_before_buffer_end', $buffer );

				$automateData = $this->getChecked();

				// allow GA cookies if set
				if( 2 === $this->getModule( 'core' )->cookiesBeforeConsent ) {
					unset( $automateData[self::AR_KEY_GA] );
					unset( $automateData[self::AR_KEY_GTAG_PREFIX . self::CC_IDX_ANALYTICAL] );
				}

				$buffer = $this->_doAutomate( $automateData, 'live', $buffer );
			} catch ( WpieExceptionInterface $e ) {
				WpieExceptionLogger::log( $e, true );
			} catch ( \Throwable $e ) {
 				WpieExceptionLogger::log( $e, true );
 			}

			/**
			 * Filter the buffer after processing
			 *
			 * @var string $buffer
			 *
			 * @since 3.2.3
			 */
			return apply_filters( 'wpca_automate_after_buffer_end', $buffer );
		}

		/**
		 * Callback for the wpca_ajax_json_return hook
		 *
		 * @param array $return
		 * @param string $action
		 * @param array $data
		 *
		 * @uses self::_getAutomateData()
		 * @uses self::_flush()
		 * @uses self::renderTable()
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		public function process( $return, $action, $data )
		{
			// if action does not match one of the following, return directly
			if( 'wpca-automate-refresh' !== $action ) {
				return $return;
			}

			$return = array();
			$old = $this->_getAutomateData();
			// flush the automate data
			$new = $this->_flush();

			// array keys
			$keysOld = array_keys( $old );
			$keysNew = array_keys( $new );

			// the newwly added items, empty array with no diff
			$diff = array_values( array_diff( $keysNew, $keysOld ) );

			// add the data to the return array
			$table = $this->renderTable();

			$return['table'] = $table;
			$return['added'] = ( count( $diff ) ) ? $diff : array();

			return $return;
		}

		/**
		 * Callback for the wpca_automate_extra hook
		 *
		 * @param array $extra
		 *
		 * @since 3.2
		 */
		public function addCategoryToAutomateTable( $extra )
		{
			$thirdParties = $this->getThirdParties();

			$extra['th'][] = __( 'Cookie Category', 'wpca' );

			foreach ( $thirdParties as $key => $thirdParty ) {
				$category = ( isset( $thirdParty['cc'] ) ) ? $thirdParty['cc'] : self::CC_IDX_DEFAULT;
				$extra['td'][$key] = $this->_cookieCategories[$category]['label'];
			}

			return $extra;
		}

		/**
		 * Callback for the wpca_init_frontend_only_hooks hook
		 *
		 * @since 3.1
		 */
		public function initHooksFrontend()
		{
			$autoErase = $this->settingsProcessor->get( 'general' )->get( 'general_automate_enable_auto_erase' );
			$enableGaAnom = $this->settingsProcessor->get( 'general' )->get( 'general_automate_enable_ga_anonymized' );
			$eraseAll = $this->settingsProcessor->get( 'general' )->get( 'general_automate_erase_all_iframes' );
			$this->_blockYouTubeNoCookie = $this->settingsProcessor->get( 'general' )->get( 'general_automate_block_youtube_no_cookie' );

			add_filter( 'wpca_script_frontend_vars_before', array( $this, 'setScriptVars' ) );

			// Do not process HTML when cookies before consent is: Google Analytics & 3rd parties
			if( 3 === $this->getModule( 'core' )->cookiesBeforeConsent ) {
				return;
			}

			if(	$autoErase ) {
				/**
				 * Let others skip live blocking
				 *
				 * @var bool $skip
				 *
				 * @since 3.1.2
				 */
				if( true === ( $skip = apply_filters( 'wpca_skip_live_blocking', false ) ) ) {
					return;
				}

				if( $eraseAll ) {
					$this->action = 'erase-all';

					/**
					 * Set the cookie category for iFrames being blocked with the "Block all iFrames" mode
					 *
					 * @var string $currentCc
					 *
					 * @since 3.2.5
					 */
					$this->_eraseAllCc = apply_filters( 'wpca_automate_block_all_iframes_cc', self::CC_IDX_DEFAULT );
					// Allow only allowed categories
					if( !$this->isAllowedCookieCategory( $this->_eraseAllCc ) ) {
						$this->_eraseAllCc = self::CC_IDX_DEFAULT;
					}
				} else {
					$this->action = 'erase';
				}

				$this->erasing = true;

				/**
				 * Let others decide to if output buffering should be started or not
				 *
				 * @var bool $do
				 *
				 * @since 3.1.2
				 */
				if( $do = apply_filters( 'wpca_do_ob_start', true ) ) {
					/**
					 * Filter the wpca_render_templates() prio
					 *
					 * Output buffering is started in the do_action( 'template_redirect' ) hook
					 *
					 * @var Number $prio
					 *
					 * @since 3.2.5
					 */
					$prio = apply_filters( 'wpca_ob_start_prio', 99999 );
					add_action( 'wpca_render_templates', array( $this, 'bufferStart' ), $prio );
				}

				if( $enableGaAnom ) {
					add_filter( 'wpca_replacement_skip', array( $this, 'skipAnonymizedGa' ), 2, 4 );
				}
			}
		}

		/**
		 * Init hooks for frontend AJAX calls
		 *
		 * @since 3.4.4
		 */
		public function initAjaxHooksFrontend() {
			// Add the Cookie Category settings box to the frontend popup
			// @since 3.2.11
			add_filter( 'wpca_popup_content', function( $content ) {
				return $content . $this->renderCookieCategorySettingsBox();
			} );
		}

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

			// set required cookies for the cookies before consent methods
			add_filter( 'wpca_cookie_categies', function( $categories ) {
				$moduleCore = $this->getModule( 'core' );
				/** @var WpcaModuleCore $moduleCore */
				if( $moduleCore->do3rdPartyCookiesBeforeConsent() ) {
					// set all categories to be required and disable them
					$categories = $this->_setCookieCategoriesRequired( array(), $categories, true );
				} elseif( $moduleCore->doGoogleAnalyticsCookiesBeforeConsent() ) {
					$categories = $this->_setCookieCategoriesRequired( array( self::CC_IDX_FUNCTIONAL, self::CC_IDX_ANALYTICAL ), $categories, true );
				}

				return $categories;
			} );

			// set cookie category data
			$this->_initCookieCategories();

			$currentTabIdx = $this->settingsProcessor->settingsPage->currentTabIdx;
			$autoErase = $this->settingsProcessor
				->get( 'general' )
				->get( 'general_automate_enable_auto_erase' );

			// ensure this logic is always triggered, also when activating due to
			// a silent upgrade
			add_action( 'wpca_activate_plugin', array( $this, 'activatePlugin' ) );

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

			if( is_admin() ) {
				if( 'general' === $currentTabIdx || 'content' === $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_automate_extra', array( $this, 'addCategoryToAutomateTable' ) );

					if(  0 === count( $this->getChecked() ) && $autoErase ) {
						$this->notice( __( 'Live blocking has been enabled but no third parties has been checked.', 'wpca' ), 'error' );
					}

					if( !$moduleCore->do3rdPartyCookiesBeforeConsent() && !$autoErase ) {
						$this->notice( sprintf( __( 'Automatic cookie blocking has been disabled. %sCookies are not automatically blocked now!%s', 'wpca' ), '<span class="wpie-red">', '</span>' ), 'error' );
					}
				}

				if( $moduleCore->doingWpcaAjaxAction ) {
					add_filter( 'wpca_ajax_json_return', array( $this,'process' ), 10, 3 );
					add_filter( 'wpca_automate_extra', array( $this, 'addCategoryToAutomateTable' ) );
				}

				if( !WpieAjaxHelper::doingAjax() ) {
					add_filter( 'wpca_validated_data_wpca_settings_general', array( $this, 'validated' ), 10, 2 );
					add_filter( 'wpca_settings_skip_fields', array( $this, 'skipSatinizing' ), 10, 2 );
					add_filter( 'wpca_update_settings', array( $this, 'updateSetting' ), 10, 3 );
					add_filter( 'tiny_mce_before_init', array( $this, 'setTinyMceInit' ), 99999 );
				}
			}

			if( !is_admin() ) {
				// Skip erasing (live blocking) on the login page
				add_filter( 'wpca_skip_live_blocking', function( $skip ) {
					if( !defined( 'WPCA_ACTIVE' ) ) {
						return true;
					}

					/**
					 * Let others skip live blocking on the login page
					 *
					 * @param bool $skipLoginPage
					 *
					 * @since 3.1.2
					 */
					$skipLoginPage = apply_filters( 'wpca_skip_live_blocking_on_login_page', true );

					if( WpieMiscHelper::isLoginPage() && $skipLoginPage ) {
						return true;
					}
					return $skip;
				} );

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


		/**
		 * Get the client Cookie Categories from the cookie
		 *
		 * @access private
		 *
		 * @uses WpieCookieHelper::read
		 * @uses WpieMiscHelper::getStringParts()
		 *
		 * @since 3.2
		 */
		private function _getClientCookieCategoriesFromCookie()
		{
			/** @var WpcaModuleCore $core */
			$core = $this->getModule( 'core' );

			// read the value of cookie wpca_cc into a class member
			$cookieValueCc = WpieCookieHelper::read( $core->getCookieName( 'cc' ) );

			// return the cookie categories as an array
			return ( false !== $cookieValueCc ) ? WpieMiscHelper::getStringParts( $cookieValueCc ): array();
		}


		/**
		 * Get the regex relative local locations array
		 *
		 * @since 3.2
		 *
		 * @return array
		 */
		private function _getRegexAllowedLocalLocations()
		{
			/**
			 * Let others modify the allowed local locations array
			 *
			 * @since 3.2
			 *
			 * @var array $allowedLocations
			 */
			return apply_filters( 'wpca_automate_regex_allowed_local_locations', $this->_regexAllowedLocalLocations );
		}


		/**
		 * Get the regex pattern for a Theme or Plugin script src URL
		 *
		 * @param string $relPathAsset
		 *
		 * @uses WpieMiscHelper::getCleanUri()
		 * @uses WpieMiscHelper::escapeRegexChars()
		 *
		 * @since 3.2
		 *
		 * @return string|boolean the pattern or false on failure
		 */
		private function _getRegexPatternForLocalScript( $relPathAsset = '', $type = 'plugin' )
		{
			// return direct if no relative asset path is passed
			if( '' === ( $relPathAsset = trim( $relPathAsset ) ) || !is_string( $relPathAsset ) ) {
				return false;
			} elseif( '' === ( $type = trim( $type ) ) || !is_string( $type ) ) {
				return false;
			} else {
				// remove a preceding slash forward
				$relPathAsset = ltrim( $relPathAsset, '/' );
			}

			// get the allowed local script types
			$allowedLocations = $this->_getRegexAllowedLocalLocations();

			if( !isset( $allowedLocations[$type] ) || !is_string( $allowedLocations[$type] ) ) {
				return false;
			}

			// the relative path to the asset, e.g. wp-content/plugins
			$relPathLocal = $allowedLocations[$type];

			$homeUriClean = WpieMiscHelper::getCleanUri( WpieMiscHelper::getHomeUri() );
			$homeUriClean = WpieMiscHelper::escapeRegexChars( trailingslashit( $homeUriClean ) );
			$escapedPath = WpieMiscHelper::escapeRegexChars( trailingslashit( $relPathLocal ).$relPathAsset );
			$pattern = sprintf( $this->_regexPatternScriptSrc, $homeUriClean, $escapedPath );

			return $pattern;
		}

		/**
		 * Get the regex patterns for the passed automate data
		 *
		 * @param array $autoData
		 *
		 * @since 3.2.3
		 *
		 * @return array
		 */
		private function _getAutomateDefaultPatterns( $autoData )
		{
			$patterns = array();
			$hasS = $autoData['has_s'];
			$hasJs = $autoData['has_js'];
			$hasJsNeedle = $autoData['has_js_needle'];
			$hasHtmlElem = $autoData['has_html_elem'];

			if( $hasHtmlElem ) {
				/**
				 * Let others filter the allowed source attributes
				 *
				 * @var array $allowedSourceAttributes
				 *
				 * @since 3.2.3
				 */
				$allowedSourceAttributes = apply_filters( 'wpca_automate_allowed_src_attributes', $this->_regexAllowedSourceAttributes );

				foreach ( (array)$autoData['html_elem'] as $htmlElem ) {
					$htmlElemAttr = explode( ':', $htmlElem['attr'] );
					$htmlElemName = WpieMiscHelper::escapeRegexChars( $htmlElem['name'] );
					$htmlElemAttrName = WpieMiscHelper::escapeRegexChars( $htmlElemAttr[0] );
					$htmlElemAttrValue = WpieMiscHelper::escapeRegexChars( $htmlElemAttr[1] );

					// Set the prefix
					if( in_array( $htmlElemAttrName, $allowedSourceAttributes ) ) {
						// match the URL schema and www also for the src attribute
						$prefix = $this->_regexParts['src_scheme_www'];
					} elseif( isset( $htmlElem['regex_prefix'] ) && '' !== $htmlElem['regex_prefix']  ) {
						// if a regex_before
						$prefix = $htmlElem['regex_prefix'];
					} else {
						$prefix = '';
					}

					// set the suffix
					$suffix = ( isset( $htmlElem['regex_suffix'] ) && '' !== $htmlElem['regex_suffix'] ) ? $htmlElem['regex_suffix'] : '';

					$pattern = ( !$htmlElem['fp'] )
						? $this->_regexPatternHtmlElemWithAttr
						: (
							( $htmlElem['placeholder'] )
							? "(?P<FP_1>$this->_regexPatternHtmlElemWithAttr)"
							: "(?P<FP_0>$this->_regexPatternHtmlElemWithAttr)"
						);

					$patterns[] = sprintf( $pattern, $htmlElemName, $htmlElemAttrName, $prefix, $htmlElemAttrValue, $suffix, $htmlElemName, $htmlElemName );
				}
			}

			if( $hasS ) {
				$s = $autoData['s'];
				foreach ( (array)$s as $term ) {
					$cleanUri = WpieMiscHelper::getCleanUri( $term, true );
					$subdmain = ( '' !== $cleanUri && '.' === $cleanUri[0] ) ? '[^.]+?' : '';
					$escapedUri = WpieMiscHelper::escapeRegexChars( $cleanUri );
					// This works without 's' modifier
					$patt = sprintf( $this->_regexPatternIframe, $subdmain, $escapedUri );
					$patterns[] = $patt;
				}
			}

			if( $hasJs ) {
				$js = $autoData['js'];
				foreach ( (array)$js as $i => $script ) {
					$hasPluginUri = false;

					$s = ( isset( $script['s'] ) ) ? $script['s'] : $script;
					$dep = ( isset( $script['dep'] ) ) ? $script['dep'] : '';
					$dt = ( isset( $script['dt'] ) ) ? $script['dt'] : 0;

					$cleanUri = WpieMiscHelper::getCleanUri( $s, true );

					// get the allowed local script types
					$allowedLocations = $this->_getRegexAllowedLocalLocations();

					// first check if the needle exists in the allowed (local) locations
					if( '' !== $cleanUri && !empty( $allowedLocations ) && preg_match( '#^'.join( '|', $allowedLocations ).'#', $cleanUri ) ) {
						$hasPluginUri = true;
						$uriBegin = trailingslashit( WpieMiscHelper::getCleanUri( WpieMiscHelper::getHomeUri() ) );

					// then check for sub domains indicated by a preceding "." (dot)
					} elseif( '' !== $cleanUri && '.' === $cleanUri[0] ) {
						$uriBegin = '[^.]+?';

					// else, no need for a special begin of the URL
					} else {
						$uriBegin = '';
					}

					$escapedUri = WpieMiscHelper::escapeRegexChars( $cleanUri );
					if( $hasPluginUri ) {
						$uriBegin = WpieMiscHelper::escapeRegexChars( $uriBegin );
					}

					$patt = sprintf( $this->_regexPatternScriptSrc, $uriBegin, $escapedUri );

					if( $dep !== '' && $dt >= 0 ) {
						$patt = "(?P<DEPJS_$i>$patt)";
					}
					$patterns[] = $patt;
				}
			}

			if( $hasJsNeedle ) {
				$jsNeedle = $autoData['js_needle'];
				foreach ( (array)$jsNeedle as $i => $needle ) {
					$s = ( isset( $needle['s'] ) ) ? $needle['s'] : $needle;
					$dep = ( isset( $needle['dep'] ) ) ? $needle['dep'] : '';
					$dt = ( isset( $needle['dt'] ) ) ? $needle['dt'] : 0;

					$escaped = WpieMiscHelper::escapeRegexChars( $s );
					$patt = sprintf( $this->_regexPatternScriptHasNeedle, $escaped );

					if( $dep !== '' && $dt >= 0 ) {
						$patt = "(?P<DEPJSN_$i>$patt)";
					}
					$patterns[] = $patt;
				}
			}

			return $patterns;
		}

		/**
		 * Set given categories as required
		 *
		 * @param array $required
		 * @param array $categories
		 * @param boolean $disable
		 *
		 * @uses self::getAllowedCookieCategories()
		 *
		 * @since 3.2
		 *
		 * return array
		 */
		private function _setCookieCategoriesRequired( $required = array(), $categories = array(), $disable = false )
		{
			if( is_array( $required ) && 0 === count( $required ) ) {
				$required = $this->getAllowedCookieCategories();
			}

			foreach ( (array)$categories as $k => $data ) {
				if( in_array( $k, $required ) ) {
					// set the category as disabled and required
					$categories[$k]['required'] = true;
					if( $disable ) {
						$categories[$k]['disabled'] = true;
					}
				}
			}

			return $categories;
		}


		/**
		 * Set given categories as checked
		 *
		 * @param array $checked
		 * @param array $categories
		 *
		 * @uses self::getClientCookieCategories()
		 *
		 * @since 3.2
		 *
		 * return array
		 */
		private function _setCookieCategoriesChecked( $checked = array(), $categories = array() )
		{
			if( is_array( $checked ) && 0 === count( $checked ) ) {
				$checked = $this->getClientCookieCategories();
			}

			foreach ( (array)$categories as $k => $data ) {
				// set the category as checked
				$categories[$k]['checked'] = ( in_array( $k, $checked ) );
			}

			return $categories;
		}


		/**
		 * Init the Cookie Category data
		 *
		 * @access private
		 *
		 * @uses self::getClientCookieCategories()
		 * @uses self::hasClientCookieCategories()
		 *
		 * @since 3.2
		 */
		private function _initCookieCategories()
		{
			// the default cookie categies array
			$cookieCategoriesDefault = array();
			foreach ( $this->_allowedCookieCategories as $idx ) {
				switch ( $idx ) {
					case self::CC_IDX_FUNCTIONAL:
					    $cookieCategoriesDefault[$idx] = array( 'label' => __( 'Functional', 'wpca' ), 'descr' => __( 'Our website uses functional cookies. These cookies are necessary to let our website work. ', 'wpca' ), 'disabled' => true, 'checked' => true, 'required' => true );
						break;
					case self::CC_IDX_ANALYTICAL:
					    $cookieCategoriesDefault[$idx] = array( 'label' => __( 'Analytical', 'wpca' ), 'descr' => __( 'Our website uses analytical cookies to make it possible to analyze our website and optimize for the purpose of a.o. the usability.', 'wpca' ), 'disabled' => false, 'checked' => true, 'required' => false );
						break;
					case self::CC_IDX_SOCIAL_MEDIA:
					    $cookieCategoriesDefault[$idx] = array( 'label' => __( 'Social media', 'wpca' ), 'descr' => __( 'Our website places social media cookies to show you 3rd party content like YouTube and FaceBook. These cookies may track your personal data.', 'wpca' ), 'disabled' => false, 'checked' => true, 'required' => false );
						break;
					case self::CC_IDX_ADVERTISING:
					    $cookieCategoriesDefault[$idx] = array( 'label' => __( 'Advertising', 'wpca' ), 'descr' => __( 'Our website places advertising cookies to show you 3rd party advertisements based on your interests. These cookies may track your personal data.', 'wpca' ), 'disabled' => false, 'checked' => true, 'required' => false );
						break;
					case self::CC_IDX_OTHER:
					    $cookieCategoriesDefault[$idx] = array( 'label' => __( 'Other', 'wpca' ), 'descr' => __( 'Our website places 3rd party cookies from other 3rd party services which aren\'t Analytical, Social media or Advertising.', 'wpca' ), 'disabled' => false, 'checked' => true, 'required' => false );
						break;
					default:
						break;
				}
			}

 			/**
 			 * Let others modify the cookie categies array
 			 *
 			 * @since 3.2
 			 *
 			 * @var array $cookieCategories
 			 */
 			$cookieCategories = apply_filters( 'wpca_cookie_categies', $cookieCategoriesDefault );

 			$this->_clientCookieCategories = $this->getClientCookieCategories();
 			$this->_hasClientCookieCategories = ( 0 < count( $this->_clientCookieCategories ) );

 			// check for client categories
 			if( $this->hasClientCookieCategories() ) {
 				foreach ( $cookieCategories as $k => $data ) {
 					// check the category that is active for the client
 					$cookieCategories[$k]['checked'] = ( in_array( $k, $this->_clientCookieCategories ) ) ? true : false;
 				}
 			}

 			// extra check on integrity of the cookie categies array: should be an array with count > 0
 			if( !is_array( $cookieCategories ) || 0 === count( $cookieCategories ) ) {
 				$cookieCategories = $cookieCategoriesDefault;
 			}

 			// extra check on integrity of the cookie categies array: should only contain allowed categories
 			foreach ( $cookieCategories as $k => $data ) {
 				if( !in_array( $k, $this->_allowedCookieCategories ) ) {
 					$cookieCategories = $cookieCategoriesDefault;
 					break;
 				}
 			}

 			// force functional category to be disabled
 			if( !$cookieCategories[self::CC_IDX_FUNCTIONAL]['disabled'] ) {
 				$cookieCategories[self::CC_IDX_FUNCTIONAL]['disabled'] = true;
 			}

 			// force functional category to be checked
 			if( !$cookieCategories[self::CC_IDX_FUNCTIONAL]['checked'] ) {
 				$cookieCategories[self::CC_IDX_FUNCTIONAL]['checked'] = true;
 			}

 			// force functional category to be required
 			if( !$cookieCategories[self::CC_IDX_FUNCTIONAL]['required'] ) {
 				$cookieCategories[self::CC_IDX_FUNCTIONAL]['required'] = true;
 			}

 			$this->_cookieCategories = $cookieCategories;

 			foreach ( $this->_cookieCategories as $k => $data ) {
 				if( $data['required'] ) {
 					$this->_requiredCookieCategories[] = $k;
 				}
 				if( !isset( $data['label'] ) ) {
 				    $this->_cookieCategories[$k]['label'] = str_replace( '-', ' ', $k );
 				}
 				if( !isset( $data['descr'] ) ) {
 				    $this->_cookieCategories[$k]['descr'] = '';
 				}
 			}
		}


		/**
		 * Re init the thirdparty entries
		 *
		 * @access private
		 *
		 * @uses self::getThirdParties() if data doest not exist in the wp_options settings entry
		 * @uses self::getChecked()
		 * @uses self::_getAutomateData()
		 *
		 * @since 2.2.7
		 * @since 3.2 return saved automate data
		 *
		 * @return array
		 */
		private function _flush()
		{
			$automateData = $this->getThirdParties();
			$checked = $this->getChecked();

			foreach ( $automateData as $key => $party ) {
				if( isset( $checked[$key] ) && $checked[$key] )
					$automateData[$key]['check'] = true;
				else
					$automateData[$key]['check'] = false;
			}

			// sort by key before storing into DB
			ksort( $automateData );

			$this->settingsProcessor->get('general')->set( 'automate_data', $automateData, true );

			// why dont return $automateData here directly?
			return $this->_getAutomateData();
		}


		/**
		 * Get the automate data
		 *
		 * @access 	private
		 *
		 * @uses 	WpcaAutomate::getThirdParties() if data doest not exist in the wp_options settings entry
		 *
		 * @since 	2.2.7
		 *
		 * @return 	array
		 */
		private function _getAutomateData()
		{
			$hasData = false;
			if( $this->settingsProcessor->get( 'general' )->has( 'automate_data' ) ) {
				$automateData = $this->settingsProcessor->get( 'general' )->get( 'automate_data' );

				if( !is_array( $automateData ) || ( is_array( $automateData ) && empty( $automateData ) ) ) {
					$hasData = false;
				} else {
					$hasData = true;
				}
			}

			if( !$hasData ) {
				$automateData = $this->getThirdParties();
				foreach ( $automateData as $key => $party ) {
					$automateData[$key]['check'] = true;
				}
				$this->settingsProcessor->get( 'general' )->set( 'automate_data', $automateData, true );
			}

			// @todo add hook to filter $automateData before return

			return $automateData;
		}


		/**
		 * Define the automate regex patterns
		 *
		 * @since 3.1.2
		 */
		private function _defineRegex()
		{
			$this->_regexParts = array(
					'-lookbehind_img'       => '(?<!src=")',
					'-lookbehind_link'      => '(?<!href=")',
					'-lookbehind_link_img'  => '(?<!href=")(?<!src=")',
					'-lookbehind_shortcode' => '(?<!])',
					'-lookbehind_after_body'=> '(?<=\<body\>)',
					'-lookahead_body_end'   => '(?=.*\</body\>)',
					'-lookahead_head_end'   => '(?=.*\</head\>)',
					'random_chars'          => '[^\s\["\']+',
					'src_scheme_www'        => '(?:https?://|//)?(?:[www\.]{4})?'
			);

			// Regex to match a script tag
			$this->_regexPatternScriptBasic = '\<script'.    // start script tag
                                              '.+?'.         // match any character (1 or more ungreedy)
                                              '\</script\>'; // end script tag

			// Regex to match an iFrame tag
			$this->_regexPatternIframeBasic = '\<iframe'.    // start iframe tag
                                              '.+?'.         // match any character (1 or more ungreedy)
                                              '\</iframe\>'; // end iframe tag

			// Regex to match a script opening tag
			$this->_regexPatternScriptTagOpen = '\<script[^\>]*?\>';

			// Regex to match a script opening tag
			$this->_regexPatternScriptTagClose = '\</script\>';

			// Regex to match a script tag and inner content
			// alternative: \<script[^\>]*?\>((?=.*?\</script\>).*?)\</script\>
			$this->_regexPatternScriptAllAdvanced = '\<script'.        // start script tag
                                                    '[^>]*?'.          // match everything (0 or more ungreedy) inside the script tag except the ending ">"
                                                    '\>'.              // match the end of the starting script tag: ">"
                                                    '('.               // start matching group
                                                    '(?!\</script\>)'. // negative lookahead for ending </script> tag
                                                    '.*?'.             // match any character (0 or more ungreedy) but not followed by an ending script tag: </script>
                                                    ')'.               // end matching group
                                                    '?'.               // matching group can occur 0 or 1
                                                    '\</script\>';     // end script tag

			// Regex to match a script tag and inner content with a certain needle
			// Note that for this regex to work all "<" characters inside the script tag should be 'masked'
			//
			// Alternatives:
			// 723 steps: \<script[^>]*?\>(?!\</script>)[^<]*%s[^<]*\</script\>
			// 817 steps: \<script[^>]*?\>(?=[^<]*\</script\>)(?=[^<]*%s)[^<]*\</script\>
			// 819 steps: \<script[^>]*?\>(?<foo>(?=[^<]*%s)(?=[^<]*\</script\>))(?(foo)[^<]*)\</script\>
			// 1146 steps: \<script[^>]*?\>(?=[^<]*\</script\>)(?=[^<]*(%s))[^<]*\</script\>

			$this->_regexPatternScriptHasNeedle = '(?P<EL_START>'.          // start script tag referenced by EL_START
                                                    '\<script'.             // match start script tag
                                                  ')'.                      // end named matching group EL_START
                                                  '[^>]*?'.                 // match everything (0 or more ungreedy) inside the script tag except the ending ">"
                                                  '\>'.                     // match the end of the starting script tag: ">"
                                                  '(?!\</script>)'.         // negative lookahead for ending </script> tag
                                                  '[^<]*'.                  // match any character (not a "<") but not followed by an ending script tag: </script>
                                                  '%s'.                     // match the 3rd party needle
                                                  '[^<]*'.                  // match any character (o or more) but not a "<"
                                                  '\</script\>';            // end script tag

			// Regex to match all script src="" tags
			$this->_regexPatternScriptSrc = '(?P<EL_START>'.                // start named matching group EL_START
                                              '\<script'.                   // match start script tag
                                            ')'.                            // end named matching group EL_START
                                            '[^>]+?'.                       // match everything (1 or more ungreedy) until the src attribute
                                              'src='.                       // match src
                                              '(?:"|\')'.                   // match opening " or '
                                                '(?P<EL_SRC>'.              // start named matching group EL_SRC
                                                  '(https?:)?'.             // match an URL with http or https (optional)
                                                  '//(?:[www\.]{4})?'.      // match www. (optional)
                                                  '%s'.                     // match an optional subdomain
                                                  '%s'.                     // match the src URL needle
                                                  '[^\s"\']*?'.             // match anything after the neelde until the " or '
                                                ')'.                        // end named matching group EL_SRC
                                              '(?:"|\')'.                   // match closing " or '
                                              '[^>]*'.                      // match any character (o or more) but not a ">"
                                              '\>'.                         // match the end of the starting script tag: ">"
                                              '[^<]*'.                      // match any character but not a "<"
                                            '\</script\>';                  // match end script tag

			// Regex to match an iframe tag for src attribute needle
            $this->_regexPatternIframe = '(?P<EL_START>'.                   // start named matching group EL_START
                                           '\<iframe'.                      // match start iframe tag
                                         ')'.                               // end named matching group EL_START
                                         '[^>]+?'.                          // match everything (1 or more ungreedy) until the src attribute
                                           'src='.                          // match src=
                                           '(?<Q>"|\')'.                    // start named matching group Q to match opening " or ' quotes
                                             '(?P<EL_SRC>'.                 // start named matching group EL_SRC
                                               '(https?://|//)?'.           // match a URL with http or https (optional) or //
                                               '(?:[www\.]{4})?'.           // match www. (optional)
                                               '%s'.                        // match an optional subdomain
                                               '%s'.                        // match the src URL needle
                                               '(?:(?!\g{Q}).)*?.'.         // match anything after the neelde until the " or '
                                             ')'.                           // end named matching group EL_SRC
                                           '\g{Q}'.                         // match closing " or '
                                           '[^>]*'.                         // match any character (o or more) but not a ">"
                                           '\>'.                            // match the end of the starting iframe tag: ">"
                                           '(?:'.                           // start passive matching group
                                           '(?!\<iframe).*?'.               // match all between the iframe tags
                                           ')'.                             // end passive matching group
                                         '\</iframe\>';                     // match end iframe tag

			$this->_regexPatternHtmlElemWithAttr = '(?P<EL_START>'.         // start named matching group EL_START
                                                     '\<%s'.                // match start tag
                                                   ')'.                     // end named matching group EL_START
                                                   '[^>]+?'.                // match everything (1 or more ungreedy) until the attribute
                                                     '%s='.                 // match attribute name
                                                     '(?:"|\')'.            // match opening " or '
                                                       '(?<ATTR_V>'.        // start named matching group ATTR_V
                                                         '%s'.              // match an optional prefix
                                                         '%s'.              // match the attribute value
														 '%s'.              // match an optional suffix
                                                       '[^"\']*?'.          // match anything after the neelde until the " or '
                                                       ')'.                 // end named matching group ATTR_V
                                                     '(?:"|\')'.            // match closing " or '
                                                     '[^>]*'.               // match any character (o or more) but not a ">"
                                                     '(?:'.                 // start matching group
                                                       '\>'.                // match the end of the starting tag: ">"
                                                       '('.                 // start passive matching group
                                                       '(?!\<%s).*?'.       // match all between the tags
                                                       ')'.                 // end passive matching group
                                                       '\</%s\>'.           // match end tag
                                                       '|'.                 // OR
                                                       '/\>'.               // match a "/>" type ending of the tag
                                                     ')';                   // end matching group

		}


		/**
		 * Callback for preg_replace_callback with context 'head'
		 *
		 * Handles the replacement of matched found inside the HTML <head></head>
		 *
		 * @access	private
		 *
		 * @param	array	$matches
		 *
		 * @since 	2.3
		 *
		 * @return 	string
		 */
		private function _eraseReplaceCbHead( $matches )
		{
				$match = $matches[0];
				$type = $this->_currentType;
				$cc = $this->_currentCc;
				$currentData = $this->_currentData;

				if( false !== strpos( $match, '<!--[wpca_skip]-->' ) ) {
					return $match;
				} elseif ( false !== strpos( $match, '<!--[wpca_mrkd]-->' ) ) {
					return $match;
				// do not block our own data script
				} elseif( false !== strpos( $match, 'var '.$this->globals->get( 'jsNamespace' ) ) ) {
					return $match;
				}

				/**
				 * Filter the $skip param
				 *
				 * If $skip is set to true, the original content for the current match will be returned
				 *
				 * @param 	bool	$skip
				 * @param 	string	$type
				 * @param 	string	$context
				 * @param 	string	$match
				 * @param 	string	$cc
				 *
				 * @since	2.3
				 *
				 * @return 	bool, default false
				 */
				$skip = apply_filters( 'wpca_replacement_skip', false, $type, 'head', $match, $cc );

				if( $skip ) {
					$newLine = $this->_replacePairs["\n"];
					return '<!--[wpca_skip]-->' . $newLine . $match;
				} else {
					/**
					 * Filter the replacement content
					 *
					 * If $skip is set to true, the original content for the current match will be returned
					 *
					 * @param 	string	$content
					 * @param 	string	$type
					 * @param 	string	$context
					 * @param 	string	$match
					 * @param 	string	$cc
					 * @param 	array	$matches
					 * @param   array   $currentData
					 *
					 * @since	2.3
					 * @since   3.2.15 added the $currentData argument
					 *
					 * @return 	string
					 */
					return apply_filters( 'wpca_replacement_text', '', $type, 'head', $match, $cc, $matches, $currentData );
				}
		}


		/**
		 * Callback for preg_replace_callback with context 'body'
		 *
		 * Handles the replacement of matched found inside the HTML <body></body>
		 *
		 * @access	private
		 *
		 * @param	array	$matches
		 *
		 * @since 	2.3
		 *
		 * @return 	string
		 */
		private function _eraseReplaceCbBody( $matches )
		{
				$match = $matches[0];
				$type = $this->_currentType;
				$cc = $this->_currentCc;
				$txt = $this->_currentTxt;
				$currentData = $this->_currentData;

				if( false !== strpos( $match, '<!--[wpca_skip]-->' ) ) {
					return $match;
				} elseif ( false !== strpos( $match, '<!--[wpca_mrkd]-->' ) ) {
					return $match;
				} elseif( false !== strpos( $match, 'var '.$this->globals->get( 'jsNamespace' ) ) ) {
					return $match;
				}

				/**
				 * Filter the $skip param
				 *
				 * @since 3.4.9
				 *
				 * @return bool, default false
				 */
				$skip = apply_filters( 'wpca_replacement_skip', false, $type, 'body', $match, $cc );

				if( $skip ) {
					$newLine = $this->_replacePairs["\n"];
					return '<!--[wpca_skip]-->' . $newLine . $match;
				} else {
					/**
					 * Filter the replacement content
					 *
					 * If $skip is set to true, the original content for the current match will be returned
					 *
					 * @param 	string	$content
					 * @param 	string	$type
					 * @param 	string	$context
					 * @param 	string	$match
					 * @param 	string	$cc
					 * @param   array   $currentData
					 *
					 * @since	2.3
					 * @since   3.2.15 added the $currentData argument
					 *
					 * @return 	string
					 */
					return apply_filters( 'wpca_replacement_text', $txt, $type, 'body', $match, $cc, $matches, $currentData );				
				}
		}


		/**
		 * Handles the automate logic for erasing
		 *
		 * @access 	private
		 *
		 * @param 	array 	$patterns
		 * @param 	string  $modifiers
		 * @param 	string 	$type
		 * @param 	array   $parts
		 *
		 * @since 	2.2.7
		 *
		 * @return	array the $parts array
		 */
		private function _erase( $patterns = '', $modifiers = '', $type = null, $parts = array() )
		{
			$newLine = WpieMiscHelper::escapeRegexChars( $this->_replacePairs["\n"] );
			$prefix = '(?:\<!--\[wpca_skip]--\>'.$newLine.')?';
			$suffix = '(?:\<!--\[wpca_mrkd]--\>)?';
			$wrapperPattern = '#'.$prefix.'%s'.$suffix.'#'.$modifiers;

			$pattern = $replacement = array();
			foreach ( $patterns as $pttrn ) {
				$pattern[] = sprintf( $wrapperPattern, $pttrn );
			}

			if( !isset( $parts['head'] ) || !isset( $parts['body'] ) ) {
				throw new WpieInvalidArgumentException( 'Parts array is not valid for '.$type.': head or body entry not found.' );
			}

			// Handle the replacement of matched found inside the HTML <head> tag
			$parts['head'] = preg_replace_callback( $pattern, array( $this, '_eraseReplaceCbHead' ), $parts['head'] );

			// if errors occured, throw an Exception
			if( null === $parts['head'] ) {
				throw new WpieRuntimeException( 'An error occured calling preg_replace_callback() context head.' );
			}

			$prefix = '((?:\<!--\[wpca_skip]--\>'.$newLine.')?';
			$suffix = '(?:\<!--\[wpca_mrkd]--\>)?)';
			$wrapperPattern = '#'.$prefix.'%s'.$suffix.'#'.$modifiers;

			$pattern = $replacement = array();
			foreach ( $patterns as $pttrn ) {
				$pattern[] = sprintf( $wrapperPattern, $pttrn );
			}

			// Handle the replacement of matched found inside the HTML <body> tag
			$parts['body'] = preg_replace_callback( $pattern, array( $this, '_eraseReplaceCbBody' ), $parts['body'] );

			// if errors occured, throw an Exception
			if( null === $parts['body'] ) {
				throw new WpieRuntimeException( 'An error occured calling preg_replace_callback() context body.' );
			}

			return $parts;
		}


		/**
		 * Handle the automate process
		 *
		 * @access 	private
		 *
		 * @param 	array 	$patterns
		 * @param 	string  $modifiers
		 * @param 	string 	$type
		 * @param 	array 	$autoData
		 * @param 	array 	$parts
		 *
		 * @uses 	wpca_map_term_to_level()
		 * @uses 	preg_replace()
		 *
		 * @throws 	Exception if an error occurs in the preg_replace() function, e.a if the output is null
		 *
		 * @since 	2.2.7
		 *
		 * @return array the modified content for head and body
		 */
		private function _automate( $patterns = '', $modifiers = '', $type = null, $autoData = array(), $parts = array() )
		{
			/**
			 * Let others modify the regex patterns
			 *
			 * @param	array	$patterns
			 * @param	string	self::$action
			 * @param	string	$type
			 *
			 * @since	2.3
			 *
			 * @return	array, the filtered patterns array
			 */
			$patterns = apply_filters( 'wpca_automate_patterns', $patterns, $this->action, $type );

			switch ( $this->action ) {
				case 'erase':
				case 'erase-all':
					return $this->_erase( $patterns, $modifiers, $type, $parts );
					break;
				default:
					throw new WpieRuntimeException( sprintf( __( "Automate 'action' is unknow.", 'wpca' ) ) );
					break;
			}
		}

		/**
		 * Handle the automate process as default
		 *
		 * @access private
		 *
		 * @param string $type
		 * @param array $autoData
		 * @param array $parts
		 *
		 * @uses WpcaAutomate::_getAutomateDefaultPatterns()
		 * @uses WpcaAutomate::_automate()
		 *
		 * @since 2.3
		 *
		 * @return array the modified content for head and body
		 */
		private function _automateDefault( $type = null, $autoData = array(), $parts = array() )
		{
		    $patterns = $this->_getAutomateDefaultPatterns( $autoData );

		    // custom HTML elememts can consume line breaks. Treat them as a single line
		    $modifiers = ( true === $autoData['has_html_elem'] ) ? 's' : '';

			return $this->_automate( $patterns, $modifiers, $type, $autoData, $parts );
		}


		/**
		 * Erase all Iframes within the body tag
		 *
		 * @param array $parts
		 *
		 * @since 3.0.2
		 * @since 3.1 also matches iFrames with inner HTML
		 *
		 * @return array the modified content for head and body
		 */
		private function _automateAll( $type = null, $autoData = array(), $parts = array() )
		{
			$patterns = array(
				sprintf( $this->_regexPatternIframe, '', '' )
			);

			return $this->_automate( $patterns, '', $type, $autoData, $parts );
		}


		/**
		 * Handle the automate process for Twitter
		 *
		 * @access 	private
		 *
		 * @param 	string 	$type
		 * @param 	array 	$autoData
		 * @param 	array 	$parts
		 *
		 * @uses WpcaAutomate::_automate()
		 *
		 * @since 2.3
		 *
		 * @return array the modified content for head and body
		 */
		private function _automateTwitter( $type = null, $autoData = array(), $parts = array() )
		{
			$patterns = $this->_getAutomateDefaultPatterns( $autoData );

			// the regex for the Twitter blockquote or link
			$regexAorBlock = '\<(?P<EL_START>blockquote|a)'.          // match start blockquote OR a tag
                               '[^>]+?'.                              // match everything (1 or more ungreedy) until the class attribute
                               'class='.                              // match class=
                               '(?:"|\')'.                            // match opening " or ' passive
                                 'twitter-(?:tweet|timeline)'.        // match the class twitter-tweet OR twitter-timeline
                               '(?:"|\')'.                            // match closing " or ' passive
                               '[^>]*'.                               // match any character (o or more) but not a ">"
                               '\>'.                                  // match the end of the starting blockquote OR a tag: ">"
                               '(?:'.                                 // start passive group
                                 '(?!\<(?P=EL_START)).*?'.            // negative lookahead to match all between blockquote (OR a) begin and end tag
                               ')'.                                   // end passive matching group
                             '\</(?P=EL_START)\>';                    // match end blockquote OR a tag

			// the regex for the Twitter script
			// @TODO: Can this be moved to main array?
			$regexScript = sprintf( $this->_regexPatternScriptSrc, '', WpieMiscHelper::escapeRegexChars( 'platform.twitter.com' ) );

			$patterns[] = $regexAorBlock;
			$patterns[] = $regexScript;

			// use the "s" flag because the <blockquote> is not pre processed for \n
			return $this->_automate( $patterns, 's', $type, $autoData, $parts );
		}


		/**
		 * Handle the automate process for Instagram
		 *
		 * @access 	private
		 *
		 * @param 	string 	$type
		 * @param 	array 	$autoData
		 * @param 	array 	$parts
		 *
		 * @uses WpcaAutomate::_automate()
		 *
		 * @since 2.3
		 *
		 * @return array the modified content for head and body
		 */
		private function _automateInstagram( $type = null, $autoData = array(), $parts = array() )
		{
			$patterns = $this->_getAutomateDefaultPatterns( $autoData );

			$patterns[] =
					// start blockquote
					'(?P<EL_START>'.
					  '\<blockquote'.
					')'.
					'[^<]+?'. // match everything except <. Using a dot (.+?) won't work
					'class='.
				    '(?:"|\')'.
				      'instagram-media'.
				      '[^"\']*?'.
				    '(?:"|\')'.
					'[^>]*?'.
					'\>'.
					'.+?'.
					'\</blockquote\>';

			// e.g. platform.instagram.com/xx_XX/embeds.js
			$patterns[] =
					// start script tag
					'(?P<EL_START>'.
					  '\<script'.
					')'.
					'[^>]+'.
					  'src='.
					  '(?:"|\')'.
					    '(?P<EL_SRC>'.
					      '(https?:)?//'.
					      '(platform|www)'.
					      '\.instagram\.com[^\s"\']+'.
					    ')'.
					  '(?:"|\')'.
					  '[^>]*'.
					  '\>'.
					  '\s*'.
					'\</script\>';

			// use the "s" flag because the <blockquote> is not pre processed for \n
			return $this->_automate( $patterns, 's', $type, $autoData, $parts );
		}


		/**
		 * Handle the automate process for YouTube
		 *
		 * @access 	private
		 *
		 * @param 	string 	$type
		 * @param 	array 	$autoData
		 * @param 	array 	$parts
		 *
		 * @uses WpcaAutomate::_automate()
		 *
		 * @since 2.2.7
		 *
		 * @return array the modified content for head and body
		 */
		private function _automateYoutube( $type = null, $autoData = array(), $parts = array() )
		{
			$patterns = $this->_getAutomateDefaultPatterns( $autoData );

			// the regex for YouTube iframes
			$assertion = $this->_blockYouTubeNoCookie ? '?:' : '?!';
			$regexIframe = sprintf( $this->_regexPatternIframe, '', 'youtube(' . ( $assertion . '-nocookie)'  ) . '?\.com/(?:embed|v)/[^\s"\']+' );
			$patterns[] = $regexIframe;

			// Plugin: https://wordpress.org/plugins/youtube-embed-plus/
			if( false !== ( $pattern = $this->_getRegexPatternForLocalScript( 'youtube-embed-plus/scripts' ) ) ) {
				$patterns[] = $pattern;
			}

			return $this->_automate( $patterns, '', $type, $autoData, $parts );
		}


		/**
		 * Handle the automate process for Vimeo
		 *
		 * @access 	private
		 *
		 * @param 	string 	$type
		 * @param 	array 	$autoData
		 * @param 	array 	$parts
		 *
		 * @uses WpcaAutomate::_automate()
		 *
		 * @see http://embed.ly/provider/vimeo for URL Schemas
		 *
		 * @since 2.2.7
		 *
		 * @return array the modified content for head and body
		 */
		private function _automateVimeo( $type = null, $autoData = array(), $parts = array() )
		{
			$patterns = $this->_getAutomateDefaultPatterns( $autoData );

			// the regex for Vimeo iframes
			$regexIframe = sprintf( $this->_regexPatternIframe, '', '(?:player.)?vimeo\.com/video/[^\s"\']+' );
			$patterns[] = $regexIframe;

			return $this->_automate( $patterns, '', $type, $autoData, $parts );
		}

		/**
		 * Do logic before automating
		 *
		 * @access 	private
		 *
		 * @param 	string 	$content
		 *
		 * @uses    str_replace() to convert \n or \r\n (new line breaks) to _NL_ or _RNL_
		 * @uses    preg_replace() to convert < to _LT_
		 *
		 * @since 	2.3
		 *
		 * @return	string
		 */
		private function _beforeAutomate( $content )
		{
			$textarr = wp_html_split( $content );

			// based on wp_replace_in_html_tags()
			$changed = false;

			/**
			 * Filter the replace pairs
			 *
			 * Replaces also "<" otherwise the regex for <script type="text/javascript">var x = 10 < 20;</script> does not work
			 *
			 * @since 3.3
			 *
			 * @var array $replacePairs
			 */
			$this->_replacePairs = apply_filters( 'wpca_automate_replace_pairs', $this->_replacePairs );

			$c = count( $textarr );
			foreach ( $this->_replacePairs as $needle => $replace ) {
				foreach ( $textarr as $i => $html ) {
					if( 0 !== $i && preg_match( "#^$this->_regexPatternScriptTagOpen#", $textarr[$i-1], $m ) ) {
						if( false !== strpos( $textarr[$i], $needle ) ) {
							$textarr[$i] = str_replace( $needle, $replace, $html );
							$changed = true;
						}

						// mask the "<"
						// wp_html_split() splits on a single "<", so to also mask other occurences
						// the loop is now inside (after) the beginning <script> tag,
						// so loop over the inner script tag entries while not at the ending script tag
						if( '<' === $needle ) {
							$ii = 1;
							do {
								// look at the next position
								$iii = $i+$ii;
								// is the next entry an ending script tag?
								$nextIsScriptEnding = ( '</script>' === $textarr[$iii] );
								// does the next current entry starts with < ?
								$nextStartsWithNeedle = ( isset( $textarr[$iii][0] ) && $needle === $textarr[$iii][0] );
								// has the current entry ends with an ending script tag?
								$currHasScriptEnding = ( 1 === preg_match( '#\</script\>$#', $textarr[$iii-1] ) );

								// in the next entry, replace a starting < occurence if it's not an ending script tag
								if( $nextStartsWithNeedle && !$nextIsScriptEnding  ) {
									$textarr[$iii] = preg_replace( '#\<(?!/script\>)#', $replace, $textarr[$iii] );
								}
								$ii++;
							} while ( !$nextIsScriptEnding && !$currHasScriptEnding );
						}
					}
				}
			}

			if ( $changed ) {
				$content = implode( $textarr );
			}

			unset( $textarr );

			return $content;
		}


		/**
		 * Do logic after automating
		 *
		 * @access 	private
		 *
		 * @param 	string 	$content
		 *
		 * @uses	str_replace() to convert the replacePairs
		 *
		 * @since 	2.3
		 *
		 * @return	string
		 */
		private function _afterAutomate( $content )
		{
			$search = array_values( $this->_replacePairs );
			$replace = array_keys( $this->_replacePairs );

			return str_replace( $search, $replace, $content );
		}


		/**
		 * Split content in <head> and <body> parts
		 *
		 * @param string $content
		 *
		 * @uses preg_split();
		 *
		 * @throws RuntimeException
		 */
		private function _getHeadBodyParts( $content = '' )
		{
			$parts = array(
					'head' => '',
					'body' => '',
					'split' => ''
			);

			$patternOnlyWhitSpace = '#\</head\>\s*\<body[^\>]*?\>#s';
			$pattern = '#\</head\>([^<]*)(\<body[^\>]*?\>)#s';

			// remove any content between the </head> and <body> but allow white spaces
			// rare cases with html tags between </head> and <body> will raise an exception
			if( !preg_match( $patternOnlyWhitSpace, $content ) ) {
				$content = preg_replace( $pattern, '</head>$2', $content );
			}

			if( preg_match( $pattern, $content, $m ) ) {
				$splitted = preg_split( $pattern, $content );

				if( 2 !== count( $splitted ) ) {
					throw new WpieRuntimeException( 'Could not split content in <head> and <body> parts.' );
				}

				$parts['head'] = $splitted[0];
				$parts['body'] = $splitted[1];
				$parts['split'] = $m[0];
				unset($splitted);

				return $parts;
			}

			return false;
		}

		/**
		 * Handles the automate logic
		 *
		 * @access 	private
		 *
		 * @param 	array 	$automateData
		 * @param	string 	$context
		 * @param	string 	$content (optional)
		 *
		 * @throws 	Exception if input parameter $data is not valid
		 *
		 * @since 	2.2.7
		 *
		 * @return	string the filtered content
		 */
		private function _doAutomate( $automateData = array(), $context = 'live', $content = '' )
		{
			if( !is_array( $automateData ) ) {
				throw new WpieInvalidArgumentException( 'Parameter $automateData is not an array. No automate data available.' );
			}
			if( empty( $automateData ) && 'erase-all' !== $this->action ) {
				return $content;
			}

			$content = $this->_beforeAutomate( $content );

			if( false === ( $parts = $this->_getHeadBodyParts( $content ) ) ) {
				throw new WpieUnexpectedValueException( 'Invalid HTML found for head and body tag. Please inspect your HTML around the ending </head> tag.' );
			}

			foreach ( $automateData as $type => $autoData ) {
				if( 0 === (int)$autoData['check'] ) {
					continue;
				}	elseif( !isset( $autoData['callback'] ) ) {
					// there should be always a callback, @see WpcaAutomate::getThirdParties()
					// this condition should therefor not/never be true
					continue;
				} else {
					$callback = $autoData['callback'];
					// for intern callback, cb's should be passed as an array
					if( $autoData['internal_cb'] ) {
						$callback = array( $this, $callback );
					}
				}

				// make params available for preg_replace_callback() callback
				$this->_currentData = $autoData;
				$this->_currentType = $type;
				$this->_currentCc = $autoData['cc'];

				switch ( $context ) {
					case 'live':
					default:
						// Call the type callback to block the 3rd party content
						$parts = call_user_func_array( $callback, array( $type, $autoData, $parts ) );
						break;
				}
			}

			// erase all iframes inside the body tag
			if( 'erase-all' === $this->action ) {
				$this->_currentType = 'all';
				$this->_currentCc = $this->_eraseAllCc;

				// Call the _automateAll callback to block all 3rd party content
				$parts = call_user_func_array( array( $this, '_automateAll' ), array( 'all', array(), $parts ) );
			}

			// reset to default as the loop is done
			$this->_currentData = null;
			$this->_currentType = null;
			$this->_currentCc = null;

			// glue parts together
			$content = $parts['head'].$parts['split'].$parts['body'];

			$content = $this->_afterAutomate( $content );

			unset( $automateData );

			return $content;
		}
	}
}