<?php
/**
 * @package n3t Cookie Consent
 * @author Pavel Poles - n3t.cz
 * @copyright (C) 2021 - Pavel Poles - n3t.cz
 * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
 *
 * @noinspection PhpMultipleClassDeclarationsInspection
 **/

defined( '_JEXEC' ) or die( 'Restricted access' );

use Joomla\CMS\Application\ApplicationHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Version;
use Joomla\Registry\Registry;
use Joomla\CMS\Uri\Uri;

class plgSystemN3tCookieConsent extends CMSPlugin
{
	private const BLOCK_DESCRIPTION = 'description';
	private const BLOCK_PRIVACY = 'privacy';
	private const BLOCK_FUNCTIONAL = 'functional';
	private const BLOCK_PREFERENCES = 'preferences';
	private const BLOCK_ANALYTICS = 'analytics';
	private const BLOCK_MARKETING = 'marketing';
	private const BLOCK_UNKNOWN = 'unknown';
	private const BLOCK_HIDDEN = 'hidden';
	private const BLOCK_SYSTEM = 'system';
	private const BLOCK_CUSTOM_DESCRIPTION = 'custom_description';
	private const BLOCK_CUSTOM = 'custom';
	private const BLOCK_CONSENT = 'consent';

	private const SCAN_IGNORE = 'ignore';

	private const LOG_FILE = 'n3t_cookie_consent.php';

	/**
	 * Contains list of known Cookies
	 * @var ?array
	 * @since 4.0.0
	 */
	private $cookieList = null;

	/**
	 * Is site set as multilingual?
	 * @var bool
	 * @since 4.0.0
	 */
	private $isMultiLanguage = false;

	/**
	 * Is scan mode?
	 * @var bool
	 * @since 4.0.0
	 */
	private $isScanMode = false;

	/**
	 * Scan mode just finished?
	 * @var bool
	 * @since 4.0.0
	 */
	private $isScanModeFinished = false;

	/**
	 * Report cookie contents
	 * @var ?array
	 * @since 4.0.0
	 */
	private $reportCookie = null;

	/**
	 * Array of newly registered Cookies
	 * @var array
	 * @since 4.0.0
	 */
	private $debuggerCollectedCookies = [];

	/**
	 * Array of blocked PHP Cookies
	 * @var array
	 * @since 4.0.0
	 */
	private $debuggerBlockedCookies = [];

	/**
	 * Array of discovered iframes
	 * @var array
	 * @since 4.0.2
	 */
	private $debuggerIFrames = [];

	/**
	 * Constructor
	 * @param $subject
	 * @param $config
	 */
	public function __construct(&$subject, $config = array())
	{
		parent::__construct($subject, $config);

		$app = Factory::getApplication();

		$this->isMultiLanguage = Multilanguage::isEnabled();
		if ($app->isClient('site'))
		{
			$this->isScanMode = !!Factory::getApplication()->getSession()->get('n3tcc_scan');
			if (!$this->isScanMode && $this->getInput()->get('n3tcc_scan') === ApplicationHelper::getHash('plg_system_n3tcookiesconsent'))
			{
				$this->isScanMode = true;
				$app->getSession()->set('n3tcc_scan', true);
			}
			elseif ($this->isScanMode && $this->getInput()->get('n3tcc_scan') === '0')
			{
				$this->isScanMode = false;
				$this->isScanModeFinished = true;
				$app->getSession()->set('n3tcc_scan', false);
			}

			if ($this->isScanMode) {
				$this->loadLanguage();
				$app->enqueueMessage(Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_MODE_RUNNING', Route::_('index.php?n3tcc_scan=0')), 'warning');
			}

			$reportCookie = $this->getInput()->cookie->get($this->params->get('cookie_name', 'n3t_cc') . '_report', '', 'string');
			if ($reportCookie)
				$this->reportCookie = explode(';', $reportCookie);
		}
	}

	/**
	 * Helper function to get JInput based on Joomla version
	 *
	 * @return \Joomla\Input\Input
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function getInput(): \Joomla\Input\Input
	{
		if (Version::MAJOR_VERSION < 4)
			return Factory::getApplication()->input;
		else
			return Factory::getApplication()->getInput();
	}

	/**
	 * Loads language files
	 *
	 * @param $extension
	 * @param $basePath
	 *
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function loadLanguage($extension = '', $basePath = JPATH_ADMINISTRATOR)
	{
		$langFile = 'plg_' . $this->_type . '_' . $this->_name . '.data';
		$lang = Factory::getApplication()->getLanguage();
		if (!$lang->getPaths($langFile))
			$lang->load($langFile, JPATH_ADMINISTRATOR);

		return parent::loadLanguage($extension, $basePath);
	}

	/**
	 * Returns true if plugin should be enabled
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function isEnabled(): bool
	{
		$app = Factory::getApplication();

		return
			$app->isClient('site')
			&& $app->getDocument()->getType() === 'html'
			&& (!$this->params->get('hide_from_bots', true) || !Factory::getApplication()->client->robot);
	}

	/**
	 * Returns true if current page is privacy page
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function isPrivacyPage(): bool
	{
		$input = $this->getInput();

		if ($this->params->get('privacy_policy_type', 'menuitem') === 'menuitem' && $this->params->get('privacy_policy') === $input->get('Itemid'))
			return true;

		return false;
	}

	/**
	 * Returns true if plugin should use autorun
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function isVisible(): bool
	{
		static $disabledTmplList = null;
		if ($disabledTmplList === null) {
			$tmplList = (array) $this->params->get('disable_tmpl', [['tmpl' => 'component'], ['tmpl' => 'raw']]);
			$disabledTmplList = [];
			foreach ($tmplList as $tmpl)
				$disabledTmplList[] = ((array)$tmpl)['tmpl'];
		}

		if (in_array($this->getInput()->get('tmpl'), $disabledTmplList))
			return false;

		return true;
	}

	/**
	 * Returns true if trigger should be visible
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function isTriggerVisible(): bool
	{
		return $this->params->get('show_trigger', 1)
			&& $this->isVisible();
	}

	/**
	 * Returns current user consent
	 *
	 * @return Registry
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function getConsent(): Registry
	{
		$consent = $this->getInput()->cookie->get($this->params->get('cookie_name', 'n3t_cc'), null, 'raw');
		return new Registry($consent);
	}

	/**
	 * Returns currently allowed categories by user consent
	 *
	 * @return Registry
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function allowedCategories(): array
	{
		$categories = [];
		foreach ($this->params->get('blocks', []) as $block) {
			if ($block->type == self::BLOCK_FUNCTIONAL || $block->type == self::BLOCK_SYSTEM || $block->type == self::BLOCK_CUSTOM && $block->readonly && $block->default_enabled)
				$categories[] = $block->type;
		}

		$consent = $this->getConsent();
		$categories = array_merge($categories, $consent->get('level', []));

		return array_unique($categories);
	}

	/**
	 * Returns if cookie by given name is enabled by current user consent
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function isCookieEnabled(string $cookieName): bool
	{
		$cookieName = trim($cookieName);

		$cookies = $this->loadCookies();
		$category = null;
		foreach ($cookies as $cookie) {
			if ($cookie['is_regex'] && preg_match('~' . $cookie['name'] . '~', $cookieName)) {
				$category = $cookie['category'];
				break;
			}
			if (!$cookie['is_regex'] && $cookie['name'] == $cookieName) {
				$category = $cookie['category'];
				break;
			}
		}

		if ($category === self::BLOCK_HIDDEN)
			$category = self::BLOCK_UNKNOWN;

		if ($category === self::BLOCK_SYSTEM)
			$category = self::BLOCK_FUNCTIONAL;

		if ($category === null) {
			switch ($this->params->get('allow_unknown_cookies', 'settings')) {
				case 'allow': return true;
				case 'block': return false;
				default: $category = self::BLOCK_UNKNOWN; break;
			}
		}

		return in_array($category, $this->allowedCategories());
	}

	/**
	 * Returns translation, if exists (avoids reporting unstranslated strings)
	 *
	 * @param $text
	 *
	 * @return string
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function translateText(string $text, ?int $count = null): string
	{
		if ($text === 'SITENAME') {
			$appConfig = Factory::getConfig();
			return $appConfig->get('sitename');
		}

		static $lang = null;
		if ($lang === null)
			$lang = Factory::getApplication()->getLanguage();
		if ($count === null)
			return $lang->hasKey($text) ? Text::_($text) : $text;
		else
			return Text::plural($text, $count);
	}

	/**
	 * Returns translation of text constant, if site is multilanguage, returns the text constnt
	 *
	 * @param $text
	 *
	 * @return string
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function multilang(string $text): string
	{
		return $this->isMultiLanguage ? $text : $this->translateText($text);
	}

	/**
	 * returns array of cookies used in script
	 * @return array
	 *
	 * @since 4.0.0
	 */
	private function loadCookies(): array
	{
		if ($this->cookieList == null) {
			$cookies = [];
			foreach ($this->params->get('blocks', []) as $block) {
				if (isset($block->cookies) && !empty($block->cookies)) {
					foreach ((array)$block->cookies as $cookie) {
						$cookie = (object)$cookie;
						if ($block->alias ?? '')
							$category = $block->alias;
						else {
							$category = $block->type ?? '';
							if (!$category)
								$category = self::BLOCK_UNKNOWN;
							if ($category == self::BLOCK_HIDDEN)
								$category = self::BLOCK_UNKNOWN;
							if ($category == self::BLOCK_SYSTEM)
								$category = self::BLOCK_FUNCTIONAL;
						}
						$cookies[] = [
							'name'     => $cookie->name,
							'is_regex' => !!($cookie->regex ?? false),
							'category' => $category,
							'required' => $block->type == self::BLOCK_FUNCTIONAL || $block->type == self::BLOCK_SYSTEM || $block->type == self::BLOCK_CUSTOM && $block->readonly && $block->default_enabled,
						];
					}
				}
			}

			$this->cookieList = $cookies;
		}

		return $this->cookieList;
	}

	/**
	 * returns true, if cookie is registered, otherwise false
	 *
	 * @param   string  $cookieName
	 *
	 * @return bool
	 *
	 * @since 4.0.0
	 */
	private function isCookieRegistered(string $cookieName): bool
	{
		$cookies = $this->loadCookies();
		foreach ($cookies as $cookie) {
			if ($cookie['is_regex'] && preg_match('~' . $cookie['name'] . '~', $cookieName))
				return true;
			if (!$cookie['is_regex'] && $cookie['name'] == $cookieName)
				return true;
		}

		return false;
	}

	/**
	 * returns array containing CookieConsent script settings
	 * @return array
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function scriptOptions(): array
  {
    $appConfig = JFactory::getConfig();
    $options = [];
    if (!$this->params->get('autorun', true) || !$this->isVisible() || $this->isPrivacyPage())
      $options['autorun'] = false;
		if ((int)$this->params->get('delay', 0))
      $options['delay'] = (int)$this->params->get('delay', 0);
    $options['cookie_expiration'] = (int)$this->params->get('cookie_expiration', 395);
    if ((int)$this->params->get('cookie_necessary_only_expiration'))
      $options['cookie_necessary_only_expiration'] = (int)$this->params->get('cookie_necessary_only_expiration');
    if ($path = $this->params->get('cookie_path', $appConfig->get('cookie_path')))
      $options['cookie_path'] = $path;
    if ($domain = $this->params->get('cookie_domain', $appConfig->get('cookie_domain')))
      $options['cookie_domain'] = $domain;
		if ($this->params->get('cookie_same_site', 'Lax') != 'Lax')
      $options['cookie_same_site'] = $this->params->get('cookie_same_site');
    $options['use_rfc_cookie'] = true;
    if ($this->params->get('force_consent'))
      $options['force_consent'] = true;
    if ((int)$this->params->get('revision'))
      $options['revision'] = (int)$this->params->get('revision');
    $options['current_lang'] = 'default';
    $options['autoclear_cookies'] = true;
    if ($this->params->get('page_scripts'))
      $options['page_scripts'] = true;
		if ($this->params->get('mode', 'opt-in') != 'opt-in')
	    $options['mode'] = $this->params->get('mode');
    if ($this->params->get('remove_cookie_tables'))
      $options['remove_cookie_tables'] = true;
    if ($this->params->get('hide_from_bots', true))
      $options['hide_from_bots'] = true;

    $guiOptions = [];
    $guiOptions['consent_modal'] = [
      'layout' => $this->params->get('consent_modal_layout', 'box'),
      'position' => $this->params->get('consent_modal_position', 'bottom right'),
      'transition' => $this->params->get('consent_modal_transition', 'slide'),
      'swap_buttons' => !!$this->params->get('consent_modal_swap_buttons', true),
    ];
    $guiOptions['settings_modal'] = [
      'layout' => $this->params->get('settings_modal_layout', 'bar'),
      'position' => $this->params->get('settings_modal_position', 'right'),
      'transition' => $this->params->get('settings_modal_transition', 'slide'),
    ];
    $options['gui_options'] = $guiOptions;

    $language = [];
		$privacyLink = '';
		if ($this->params->get('privacy_policy_type', 'menuitem') === 'menuitem' && $this->params->get('privacy_policy'))
			$privacyLink = Route::_('index.php?Itemid=' . $this->params->get('privacy_policy'));
		elseif ($this->params->get('privacy_policy_type', 'menuitem') === 'url' && $this->params->get('privacy_policy_url'))
			$privacyLink = $this->params->get('privacy_policy_url');

		$consentText = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_CONSENT_MODAL_DESCRIPTION');
	  if ($this->params->get('secondary_button_role', 'settings') !== 'settings' && $this->params->get('tertiary_button_role', 'none') !== 'settings')
		  $consentText .= ' ' .  Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_SETTINGS_LINK');

		if ($privacyLink)
			$consentText .= ' ' .  Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_PRIVACY_POLICY_LINK', $privacyLink);

	  if ((int)$this->params->get('revision'))
	    $consentText .= '{{revision_message}}';

    $consentModallanguage = [
      'title' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_CONSENT_MODAL_TITLE'),
      'description' => $consentText,
      'revision_message' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_CONSENT_MODAL_REVISION'),
      'primary_btn' => [
        'text' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BTN_' . $this->params->get('primary_button_role', 'accept_all')),
        'role' => $this->params->get('primary_button_role', 'accept_all'),
      ]
    ];

    if ($this->params->get('secondary_button_role', 'settings') != 'none') {
      $consentModallanguage['secondary_btn'] = [
        'text' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BTN_' . $this->params->get('secondary_button_role', 'settings')),
        'role' => $this->params->get('secondary_button_role', 'settings'),
      ];
    }
    $language['consent_modal'] = $consentModallanguage;

    $settingsModallanguage = [
      'title' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_SETTINGS_MODAL_TITLE'),
      'save_settings_btn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BTN_SAVE_SETTINGS'),
      'accept_all_btn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BTN_ACCEPT_ALL_SETTINGS'),
      'close_btn_label' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BTN_CLOSE_SETTINGS'),
    ];
    if ($this->params->get('show_reject_all', true))
      $settingsModallanguage['reject_all_btn'] = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BTN_REJECT_ALL_SETTINGS');

    $settingsModallanguage['cookie_table_headers'] = [
      ['name' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_TABLE_NAME')],
      ['description' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_TABLE_DESCRIPTION')],
    ];

	  if ($this->params->get('show_cookie_provider', 1))
		  $settingsModallanguage['cookie_table_headers'][] = ['provider' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_TABLE_PROVIDER')];

	  if ($this->params->get('show_cookie_expiration', 1))
		  $settingsModallanguage['cookie_table_headers'][] = ['expiration' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_TABLE_EXPIRATION')];

    $settingsModallanguage['blocks'] = [];

	  if ($this->isScanMode) {
		  $blockOptions = [];
		  $blockOptions['title'] = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_SCAN_TITLE');
		  $blockOptions['description'] = Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_SCAN_DESCRIPTION', Route::_('index.php?n3tcc_scan=0'));
		  $settingsModallanguage['blocks'][] = $blockOptions;
	  }

    foreach ($this->params->get('blocks', []) as $block) {
      $blockOptions = [];
			if ($block->type == self::BLOCK_HIDDEN)
				continue;
	    if ($block->type == self::BLOCK_SYSTEM)
		    continue;

      switch ($block->type) {
	      case self::BLOCK_CUSTOM:
	      case self::BLOCK_CUSTOM_DESCRIPTION:
          $blockOptions['title'] = $this->translateText($block->title);
          $blockOptions['description'] = $this->translateText($block->description);
          if ($block->type == self::BLOCK_CUSTOM) {
            $blockOptions['toggle'] = [
              'value' => $block->alias,
              'enabled' => !!$block->default_enabled,
              'readonly' => !!$block->readonly,
            ];
          }
          break;

	      case self::BLOCK_PRIVACY:
					if ($privacyLink) {
						$blockOptions['title'] = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_' . $block->type . '_TITLE');
						$blockOptions['description'] = Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_' . $block->type . '_DESCRIPTION', $privacyLink);
					} else
						continue 2;
		      break;

	      case self::BLOCK_CONSENT:
		      $blockOptions['title'] = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_' . $block->type . '_TITLE');
		      $blockOptions['description'] = '<div class="n3tcc-consent-info">' . Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_' . $block->type . '_DESCRIPTION') . '</div>';
		      break;

        default:
          $blockOptions['title'] = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_' . $block->type . '_TITLE');
          $blockOptions['description'] = Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_BLOCK_' . $block->type . '_DESCRIPTION');
          break;
      }

      switch ($block->type) {
	      case self::BLOCK_CUSTOM:
          $blockOptions['toggle'] = [
            'value' => $block->alias,
            'enabled' => !!$block->default_enabled,
            'readonly' => !!$block->readonly,
          ];
          break;

	      case self::BLOCK_FUNCTIONAL:
          $blockOptions['toggle'] = [
            'value' => $block->type,
            'enabled' => true,
            'readonly' => true,
          ];
          break;

	      case self::BLOCK_PREFERENCES:
	      case self::BLOCK_ANALYTICS:
	      case self::BLOCK_MARKETING:
	      case self::BLOCK_UNKNOWN:
          $blockOptions['toggle'] = [
            'value' => $block->type,
            'enabled' => false,
            'readonly' => false,
          ];
          break;
      }

	    if (isset($block->cookies) && !empty($block->cookies)) {
        $cookieTable = [];
        foreach ((array)$block->cookies as $cookie) {
					$cookie = (object)$cookie;
          $cookieOptions = [
            'name' => $cookie->name,
            'description' => $this->translateText($cookie->description ?? '') ?: '&nbsp;',
            'is_regex' => !!($cookie->regex ?? false),
          ];

					if ($this->params->get('show_cookie_provider', 1))
						$cookieOptions['provider'] = $this->translateText($cookie->provider ?? '') ?: '&nbsp;';

	        if ($this->params->get('show_cookie_expiration', 1)) {
						if (isset($cookie->expiration_unit) && !empty($cookie->expiration_unit))
		          $cookieOptions['expiration'] = $this->translateText('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_' . strtoupper($cookie->expiration_unit), (int)($cookie->expiration ?? ''));
	          else
		          $cookieOptions['expiration'] = $this->translateText($cookie->expiration ?? '') ?: '&nbsp;';
	        }

          $cookieTable[] = $cookieOptions;
        }

				if ($cookieTable)
          $blockOptions['cookie_table'] = $cookieTable;
      }

      $settingsModallanguage['blocks'][] = $blockOptions;
    }

    $language['settings_modal'] = $settingsModallanguage;
    $options['languages'] = [
      'default' => $language,
    ];

    return $options;
  }

	/**
	 * Returns array containing CSS variables with custom colors
	 * @return array
	 *
	 * @since 4.0.0
	 */
	private function styleOptions(): array
  {
    $colors = [];

    foreach ($this->params as $name => $value) {
      if (preg_match('~^color_~', $name) && $value) {
        $name =  '--cc-' . preg_replace('~^color_~', '', $name);
        $name = str_replace('_', '-', $name);
        $colors[$name] = $value;
      }
    }

    return $colors;
  }

	/**
	 * returns array containing IFrameManager script settings
	 * @return array
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function iframeManagerOptions(): array
	{
		$services = [];
		$service = [
			'embedUrl' => '{data-id}',
			'cookie' => [
				'name' => $this->params->get('cookie_name', 'n3t_cc') . '_ifm_unknown',
				'expiration' => (int)$this->params->get('cookie_expiration', 395),
			],
			'languages' => [
				'default' => [
					'notice' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_UNKNOWN'),
					'loadBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_LOAD'),
					'loadAllBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_ALWAYS'),
				]
			],
		];
		if ($thumb = $this->params->get('ifm_thumbnail')) {
			if (Version::MAJOR_VERSION >= 4)
				$thumb = HTMLHelper::cleanImageURL($thumb)->url;
			$service['thumbnailUrl'] = Uri::base() . $thumb;
		}
		$services['unknown'] = $service;

		$service = [
			'embedUrl' => 'https://www.youtube-nocookie.com/embed/{data-id}',
			'thumbnailUrl' => 'https://i3.ytimg.com/vi/{data-id}/hqdefault.jpg',
			'cookie' => [
				'name' => $this->params->get('cookie_name', 'n3t_cc') . '_ifm_youtube',
			],
			'languages' => [
				'default' => [
					'notice' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_YOUTUBE'),
					'loadBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_LOAD'),
					'loadAllBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_ALWAYS'),
				]
			],
		];
		$services['youtube'] = $service;

		$service = [
			'embedUrl' => 'https://player.vimeo.com/video/{data-id}',
			'cookie' => [
				'name' => $this->params->get('cookie_name', 'n3t_cc') . '_ifm_vimeo',
			],
			'languages' => [
				'default' => [
					'notice' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_VIMEO'),
					'loadBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_LOAD'),
					'loadAllBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_ALWAYS'),
				]
			],
		];
		$services['vimeo'] = $service;

		$service = [
			'embedUrl' => 'https://www.dailymotion.com/embed/video/{data-id}',
			'cookie' => [
				'name' => $this->params->get('cookie_name', 'n3t_cc') . '_ifm_dailymotion',
			],
			'languages' => [
				'default' => [
					'notice' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_DAILYMOTION'),
					'loadBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_LOAD'),
					'loadAllBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_ALWAYS'),
				]
			],
		];
		$services['dailymotion'] = $service;

		$providers = (array)$this->params->get('ifm_services');
		foreach ($providers as $provider) {
			$provider = (array)$provider;

			$service = [
				'embedUrl' => '{data-id}',
				'cookie' => [
					'name' => $this->params->get('cookie_name', 'n3t_cc') . '_ifm_' . md5($provider['url']),
				],
				'languages' => [
					'default' => [
						'notice' => $provider['terms'] ? Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_GENERAL_TOS', $provider['terms'], $provider['name']) : Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_GENERAL', $provider['name']),
						'loadBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_LOAD'),
						'loadAllBtn' => Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_IFM_BTN_ALWAYS'),
					]
				],
			];
			if ($thumb = $provider['thumbnail']) {
				if (Version::MAJOR_VERSION >= 4)
					$thumb = HTMLHelper::cleanImageURL($thumb)->url;
				$service['thumbnailUrl'] = Uri::base() . $thumb;
			} elseif ($thumb = $this->params->get('ifm_thumbnail')) {
				if (Version::MAJOR_VERSION >= 4)
					$thumb = HTMLHelper::cleanImageURL($thumb)->url;
				$service['thumbnailUrl'] = Uri::base() . $thumb;
			}
			$services[md5($provider['url'])] = $service;
		}


		$options = [
			'currLang' => 'default',
			'services' => $services,
		];

		return $options;
	}

	/**
	 * Resturns string with HTML code to insert after <head>
	 * @return string|null
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function htmlOutput(): ?string
	{
		$html = "\n";

		$path = HTMLHelper::script('plg_n3tcookieconsent/n3tconsentmanager.min.js', ['pathOnly' => true, 'relative' => true]);
		$path .= '?' . Factory::getDocument()->getMediaVersion();
		$html.= '<script src="' . $path. '"></script>' . "\n";

		if ($this->isTriggerVisible()) {
			$trigger = $this->renderLayout('trigger', [
				'params' => $this->params,
			]);
		} else
			$trigger = false;

		$params = [
			'options' => $this->scriptOptions(),
			'cookies' => $this->loadCookies(),
			'trigger' => $trigger,
			'params' => $this->params,
			'isScanMode' => $this->isScanMode,
		];
		if ($this->params->get('use_iframe_manager', false))
			$params['iframeManagerOptions'] = $this->iframeManagerOptions();
		$script = $this->renderLayout('script', $params);
		$html.= '<script>' . $script . '</script>' . "\n";

		return $html;
	}

	/**
	 * Updates plugin settings in database
	 * @return bool
	 *
	 * @since 4.0.0
	 */
	private function updateParams(): bool
	{
		$plugin = PluginHelper::getPlugin($this->_type, $this->_name);

		if (strlen((string)$this->params) > pow(2,16) - 1)
			return false;

		$db = Factory::getDbo();
		$query = $db->getQuery(true);
		$query->update('#__extensions')
			->set('params=' . $db->quote((string)$this->params))
			->where('extension_id = ' . (int)$plugin->id);

		try
		{
			$db->lockTable('#__extensions');
		} catch (Exception $e) {
			return false;
		}

		try {
			$result = $db->setQuery($query)->execute();
		} catch (Exception $e) {
			$db->unlockTables();
			return false;
		}

		$db->unlockTables();
		$this->cookieList = null;
		return $result;
	}

	/**
	 * Parse expiration string into expiration and expiration unit
	 *
	 * @param   string  $expirationStr
	 * @param   string  $expiration
	 * @param   string  $expirationUnit
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function parseExpiration(string $expirationStr, ?string &$expiration, ?string &$expirationUnit)
	{
		$expiration = $expirationStr;
		$expirationUnit = '';
		if ($this->isMultiLanguage)
		{
			switch (true) {
				case strpos(strtolower($expiration), 'session') !== false:
					$expiration = $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_SESSION');
					break;
				case strpos(strtolower($expiration), 'various') !== false:
					$expiration = $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_VARIOUS');
					break;
				case strpos(strtolower($expiration), 'year') !== false:
					$expiration = (int)$expiration;
					$expirationUnit = 'years';
					break;
				case strpos(strtolower($expiration), 'month') !== false:
					$expiration = (int)$expiration;
					$expirationUnit = 'months';
					break;
				case strpos(strtolower($expiration), 'day') !== false:
					$expiration = (int)$expiration;
					$expirationUnit = 'days';
					break;
				case strpos(strtolower($expiration), 'hour') !== false:
					$expiration = (int)$expiration;
					$expirationUnit = 'hours';
					break;
				case strpos(strtolower($expiration), 'minute') !== false:
					$expiration = (int)$expiration;
					$expirationUnit = 'minutes';
					break;
				case strpos(strtolower($expiration), 'second') !== false:
					$expiration = (int)$expiration;
					$expirationUnit = 'seconds';
					break;
			}
		} else {
			switch (true) {
				case strpos(strtolower($expiration), 'session') !== false:
					$expiration = $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_SESSION');
					break;
				case strpos(strtolower($expiration), 'various') !== false:
					$expiration = $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_VARIOUS');
					break;
				case strpos(strtolower($expiration), 'year') !== false:
					$expiration = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_YEARS', (int) $expiration);
					break;
				case strpos(strtolower($expiration), 'month') !== false:
					$expiration = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_MONTHS', (int) $expiration);
					break;
				case strpos(strtolower($expiration), 'day') !== false:
					$expiration = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_DAYS', (int) $expiration);
					break;
				case strpos(strtolower($expiration), 'hour') !== false:
					$expiration = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_HOURS', (int) $expiration);
					break;
				case strpos(strtolower($expiration), 'minute') !== false:
					$expiration = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_MINUTES', (int) $expiration);
					break;
				case strpos(strtolower($expiration), 'second') !== false:
					$expiration = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_SECONDS', (int) $expiration);
					break;
			}
		}
	}

	/**
	 * Search cookie databases for cookie and return its description
	 *
	 * @param   string  $cookieName
	 *
	 * @return ?array
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function searchCookieDatabase(string $cookieName): ?array
	{
		$cookieData = null;

		// Joomla cookie database
		$lang = Factory::getApplication()->getLanguage();
		if (($handle = fopen(__DIR__ . '/data/joomla.csv', "r")) !== false) {
			while (($data = fgetcsv($handle)) !== false) {
				if (count($data) < 7)
					continue;
				$name = $data[1];
				$name = str_replace('(n3t_cc)', $this->params->get('cookie_name', 'n3t_cc'), $name);
				if ((int)$data[3])
					$name = ApplicationHelper::getHash($name);
				if ((int)$data[4])
					$name = md5($name);
				if (!(int)$data[2] && $cookieName !== $name)
					continue;
				if ((int)$data[2] && !preg_match('~' . $name . '~', $cookieName))
					continue;

				$cookieData = [
					'name' => $name,
					'description' => $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_DATABASE_' . $data[5]),
					'regex' => (int)$data[2],
					'provider' => $this->multilang('SITENAME'),
					'expiration'  => $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_UNKNOWN'),
					'category' => $data[6],
				];

				switch ($data[7]) {
					case 'plugin':
						$info = explode('|', $data[8]);
						if (count($info) < 5)
							break;
						$plugin = PluginHelper::getPlugin($info[0], $info[1]);
						if (!$plugin)
							break;
						$params = new Registry($plugin->params);
						if ($this->isMultiLanguage) {
							$cookieData['expiration'] = (int)$params->get($info[2], (int)$info[3]);
							$cookieData['expiration_unit'] = $info[4];
						} else
							$cookieData['expiration'] = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_' . strtoupper($info[4]), (int)$params->get($info[2], (int)$info[3]));
						break;
					case 'session':
					case 'various':
					case 'unknown':
						$cookieData['expiration'] = $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_' . strtoupper($data[7]));
						break;
					default:
						if ($this->isMultiLanguage) {
							$cookieData['expiration'] = $data[7];
							$cookieData['expiration_unit'] = $data[8];
						} else
							$cookieData['expiration'] = Text::plural('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_' . strtoupper($data[7]), (int)$data[8]);
				}

				break;
			}

			fclose($handle);
		}

		// Open cookie database
		if (!$cookieData && (($handle = fopen(__DIR__ . '/data/open-cookie-database.csv', "r")) !== false)) {
			while (($data = fgetcsv($handle)) !== false) {
				if (count($data) < 10)
					continue;
				if (!(int)$data[9] && $cookieName !== $data[3])
					continue;
				if ((int)$data[9] && strpos($cookieName, $data[3]) !== 0)
					continue;

				$description = $data[5];
				if ($lang->hasKey('PLG_SYSTEM_N3TCOOKIECONSENT_DATABASE_' . $data[0]))
					$description = $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_DATABASE_' . $data[0]);
				$this->parseExpiration($data[6], $expiration, $expirationUnit);
				$cookieData = [
					'name' => (int)$data[9] ? '^' . $data[3] : $data[3],
					'description' => $description,
					'regex' => (int)$data[9],
					'provider' => $data[1],
					'expiration' => $expiration,
					'expiration_unit' => $expirationUnit,
				];

				switch (strtolower($data[2])) {
					case 'functional': $cookieData['category'] = self::BLOCK_FUNCTIONAL; break;
					case 'analytics': $cookieData['category'] = self::BLOCK_ANALYTICS; break;
					case 'marketing': $cookieData['category'] = self::BLOCK_MARKETING; break;
					default: $cookieData['category'] = self::BLOCK_HIDDEN; break;
				}

				break;
			}

			fclose($handle);
		}

		// Unknown Cookie
		if (!$cookieData) {
			$cookieData = [
				'name'        => $cookieName,
				'description' => $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_DESCRIPTION_UNKNOWN'),
				'regex'       => 0,
				'provider'    => $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_PROVIDER_UNKNOWN'),
				'expiration'  => $this->multilang('PLG_SYSTEM_N3TCOOKIECONSENT_SCAN_COOKIE_EXPIRATION_UNKNOWN'),
				'category'    => self::BLOCK_HIDDEN,
			];
		}

		return $cookieData;
	}

	/**
	 * Adds new unknown Cookie to "Unknown" category
	 *
	 * @param   string  $cookieName
	 *
	 * @return bool
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	private function registerUnknownCookie(string $cookieName): bool
	{
		$cookieName = trim($cookieName);

		if (!$cookieName)
			return false;

		if ($this->isCookieRegistered($cookieName))
			return false;

		if ($this->isScanMode) {
			$scannedList = Factory::getApplication()->getSession()->get('n3tcc_scan_list', []);
			if (!in_array($cookieName, $scannedList)) {
				$scannedList[] = $cookieName;
				Factory::getApplication()->getSession()->set('n3tcc_scan_list', $scannedList);
			}
		}

		$this->debuggerCollectedCookies[] = $cookieName;
		$cookieData = $this->searchCookieDatabase($cookieName);
		if ($cookieData['category'] === self::SCAN_IGNORE)
			return false;

		$blocks = (array)$this->params->get('blocks', []);
		$blockIndex = null;
		foreach ($blocks as $index => $block) {
			if ($block->type == $cookieData['category'] || isset($block->alias) && $block->alias == $cookieData['category']) {
				$blockIndex = $index;
				break;
			}
		}

		if ($blockIndex !== null)
			$block = $blocks[$blockIndex];
		else {
			$block = new stdClass();
			$block->type  = $cookieData['category'];
			$block->alias = '';
			$block->title = '';
			$block->description = '';

			$block->default_enabled = 0;
			$block->readonly = 0;
			$block->cookies = [];
			$blocks[] = $block;
		}

		unset($cookieData['category']);
		if (isset($block->cookies) && !empty($block->cookies))
			$block->cookies = (array)$block->cookies;
		else
			$block->cookies = [];
		$block->cookies[] = (object)$cookieData;

		$this->params->set('blocks', $blocks);
		$this->cookieList = null;
		return true;
	}

	/**
	 * Returns log file based on params
	 *
	 * @return string
	 *
	 * @since 4.0.0
	 */
	private function logFile(): string
	{
		return $this->params->get('log_consents_rotate') ?  self::LOG_FILE : 'keep/' . self::LOG_FILE;
	}

	/**
	 * Joomla onBeforeRender Event
	 *
	 * @since 4.0.0
	 */
	public function onBeforeRender()
	{
		$app = Factory::getApplication();

		if ($this->isScanModeFinished) {
			$script = "window.parent.postMessage('n3t_cookie_consent_finish_scan', '*');";

			if (Version::MAJOR_VERSION < 4)
				Factory::getDocument()->addScriptDeclaration($script);
			else
			{
				$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
				$wa->addInlineScript($script);
			}
		}

		if (!$app->isClient('administrator'))
			return;

		$hasDefinition = false;
		$blocks = (array)$this->params->get('blocks', []);
		foreach ($blocks as $block) {
			if (isset($block->cookies) && !empty($block->cookies)) {
				$hasDefinition = true;
				break;
			}
		}

		if (!$hasDefinition) {
			$this->loadLanguage();
			$plugin = PluginHelper::getPlugin($this->_type, $this->_name);

			$app->enqueueMessage(Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_WARNING_SETUP_COOKIES_FIRST',
				Route::_('index.php?option=com_plugins&view=plugin&task=plugin.edit&extension_id=' . $plugin->id)), 'warning');
		}
	}

	/**
	 * @param   string  $attrStr
	 *
	 * @return array
	 *
	 * @since 4.0.2
	 */
	private function parseHtmlAttributes(string $htmlCode, array &$debuggerData = []): array
	{
		$dom = new \DomDocument();
		$dom->loadHTML($htmlCode);
		$elem = $dom->getElementsByTagName('iframe')->item(0);
		$attrs = [];
		foreach ($elem->attributes as $name => $value)
			$attrs[$name] = $value->textContent;

		return $attrs;
	}

	/**
	 * Joomla onAfterRender Event
	 *
	 * @since 4.0.0
	 */
	public function onAfterRender()
  {
	  $app = Factory::getApplication();
	  if (!$this->isEnabled())
			return;

	  $buffer = $app->getBody();
	  if (strpos($buffer, '<html') === false)
			return;

	  $this->loadLanguage();

		$html = $this->htmlOutput();
		if ($html)
			$buffer = preg_replace('~(<head(\s[^>]*)?>)~', '$1' . $html, $buffer);

		if (strpos($buffer, '{n3tcookieconsent ') !== false)
			$buffer = preg_replace('~{n3tcookieconsent\ssettings}([^{]*){/n3tcookieconsent}~', '<a href="#" aria-label="$1" class="cc-link" data-cc="c-settings">$1</a>', $buffer);

		if ($this->params->get('youtube_nocookie', true)) {
			if (strpos($buffer, '<iframe') !== false) {
				$buffer = preg_replace_callback('~<iframe\s[^>]*?>~', function($matches) {
					return preg_replace('~http(s)?://(www\.)?youtube\.com/~', 'http$1://www.youtube-nocookie.com/', $matches[0]);
				}, $buffer);
			}
		}

	  if ($this->params->get('use_iframe_manager', false)) {
		  if (strpos($buffer, '<iframe') !== false) {
			  $buffer = preg_replace_callback('~<iframe(\s[^>]*)?>~', function($matches) {
				  $debuggerIframe = [
					  'code' => $matches[0],
				  ];

				  $attributes = $this->parseHtmlAttributes($matches[0], $debuggerIframe);
				  $debuggerIframe['attributes'] = $attributes;

				  $service = '';
				  $id = '';
				  if (isset($attributes['src'])) {
					  $id = $attributes['src'];
					  unset($attributes['src']);
				  }

				  $debuggerIframe['processed'] = !!$id;
				  if (!$id) {
					  $this->debuggerIFrames[] = $debuggerIframe;
					  return $matches[0];
				  }

				  $uri = new Uri($id);
				  $domain = $uri->getHost();

				  if (!$domain || $domain == (new Uri(Uri::base()))->getHost()) {
					  $debuggerIframe['processed'] = false;
					  $this->debuggerIFrames[] = $debuggerIframe;
				    return $matches[0];
			    }

				  $providers = (array)$this->params->get('ifm_whitelist', []);
				  foreach ($providers as $provider) {
					  $provider = (array)$provider;
					  if (stripos($id, $provider['url']) === 0) {
						  $debuggerIframe['processed'] = false;
						  $this->debuggerIFrames[] = $debuggerIframe;
						  return $matches[0];
					  }
				  }

				  $providers = (array)$this->params->get('ifm_services', []);
				  foreach ($providers as $provider) {
					  $provider = (array)$provider;
					  if (stripos($id, $provider['url']) === 0)
						  $service = md5($provider['url']);
				  }

					if (!$service && preg_match('~^(www\.)(youtube(-nocookie)?\.com|youtu.be)$~i', $domain)) {
						$service = 'youtube';
						$id = $uri->getPath();
						$id = explode('/', $id);
						$id = array_pop($id);
					}

				  if (!$service && preg_match('~^player\.vimeo\.com$~i', $domain)) {
					  $service = 'vimeo';
					  $id = $uri->getPath();
					  $id = explode('/', $id);
					  $id = array_pop($id);
				  }

				  if (!$service && preg_match('~^(api|www)\.dailymotion\.com$~i', $domain)) {
					  $service = 'dailymotion';
					  $id = $uri->getPath();
					  $id = explode('/', $id);
					  $id = array_pop($id);
				  }

				  $service = $service ?: 'unknown';

				  $this->debuggerIFrames[] = $debuggerIframe;
				  return '<div data-service="' . $service . '" data-id="' . $id . '" data-autoscale data-attributes="' . htmlspecialchars(json_encode($attributes), ENT_QUOTES) . '"></div>';
			  }, $buffer);
		  }
	  }

	  $app->setBody($buffer);
  }

	/**
	 * n3tDebug onN3tDebugAddPanel Event
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onN3tDebugAddPanel()
	{
		if (!Factory::getApplication()->isClient('site'))
			return null;

		if (Version::MAJOR_VERSION == 3)
			JLoader::registerNamespace('n3tCookieConsent', __DIR__ . DIRECTORY_SEPARATOR);
		else
			JLoader::registerNamespace('n3tCookieConsent', __DIR__ . DIRECTORY_SEPARATOR . 'n3tCookieConsent');

		return new \n3tCookieConsent\Debug\Panel($this);
	}

	/**
	 * Helper function for debugger panel
	 *
	 * @return array
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function collectDebugData(): array
	{
		$data = [
			'consent' => $this->getConsent()->toArray(),
			'allowedCategories' => $this->allowedCategories(),
		];

		if ($this->debuggerBlockedCookies)
			$data['blockedCookies'] = $this->debuggerBlockedCookies;

		if ($this->isScanMode) {
			$data['scanMode'] = true;
			$data['reportCookie'] = $this->reportCookie;
			$data['scannedList'] = Factory::getApplication()->getSession()->get('n3tcc_scan_list', []);
			$data['collectedCookies'] = $this->debuggerCollectedCookies;
		}

		if ($this->debuggerIFrames)
			$data['iframes'] = $this->debuggerIFrames;

		return $data;
	}

	/**
	 * Joomla onBeforeCompileHead Event
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onBeforeCompileHead()
  {
	  if (!$this->isEnabled())
		  return;

	  $this->loadLanguage();

	  HTMLHelper::script('plg_n3tcookieconsent/cookieconsent.min.js', ['version' => 'auto', 'relative' => true], ['defer' => true]);
	  HTMLHelper::stylesheet( 'plg_n3tcookieconsent/n3tconsentmanager.min.css', ['version' => 'auto', 'relative' => true]);
		if ($this->params->get('use_iframe_manager', false)) {
			HTMLHelper::script('plg_n3tcookieconsent/iframemanager.min.js', ['version' => 'auto', 'relative' => true], ['defer' => true]);
		}

	  $style = $this->renderLayout('style', [
		  'styles' => $this->styleOptions(),
		  'params' => $this->params,
	  ]);
		$doc = Factory::getDocument();
		$doc->addStyleDeclaration($style);

	  if ($this->isScanMode) {
			$input = $this->getInput();
		  $needUpdate = false;

			if ($this->reportCookie) {
				foreach($this->reportCookie as $cookieName)
					$needUpdate = $this->registerUnknownCookie($cookieName) || $needUpdate;

				$appConfig = JFactory::getConfig();
				$path = $this->params->get('cookie_path', $appConfig->get('cookie_path', '/'));
				$domain = $this->params->get('cookie_domain', $appConfig->get('cookie_domain'));
				if (!$domain)
					$domain = '.' . $this->getInput()->server->get('SERVER_NAME');
				$input->cookie->set($this->params->get('cookie_name', 'n3t_cc') . '_report', '', 1, $path, $domain);
			}

		  $headers = headers_list();
		  foreach ($headers as $header) {
			  if (stripos($header, 'Set-Cookie:') === 0) {
				  $cookie = explode(';', trim(substr($header, 11)));
				  $cookie = explode('=', $cookie[0]);
				  if (count($cookie) === 2)
						$needUpdate = $this->registerUnknownCookie($cookie[0]) || $needUpdate;
			  }
		  }

		  if ($needUpdate)
			  $this->updateParams();
	  }

		// Block PHP cookies
	  $headers = headers_list();
		$needUpdate = false;
	  foreach ($headers as $index => $header) {
		  if (stripos($header, 'Set-Cookie:') === 0) {
			  $cookie = explode(';', trim(substr($header, 11)));

				foreach ($cookie as $value) {
					$value = trim($value);
					if (preg_match('~^expires=~i', $value)) {
						$value = explode('=', trim($value));
						$date = new \DateTime($value[1]);
						$now = new DateTime();
						if ($date < $now)
							continue 2;
					}
				}

			  $cookie = explode('=', $cookie[0]);
			  if (count($cookie) === 2) {
					$cookieName = trim($cookie[0]);
				  if (!$this->isCookieEnabled($cookieName)) {
						$this->debuggerBlockedCookies[] = $cookieName;
						unset($headers[$index]);
					  $needUpdate = true;
				  }
			  }
		  }
	  }

		if ($needUpdate) {
			header_remove();
			foreach ($headers as $header)
				header($header);
		}

	  // TODO add scripts based on categories
  }

	/**
	 * Joomla Ajax Event
	 * index.php?option=com_ajax&group=system&format=raw&plugin=N3tCookieConsentExport
	 * Export plugin settings in JSON format
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onAjaxN3tCookieConsentExport()
	{
		$app = Factory::getApplication();
		if (!$app->isClient('administrator'))
			return;

		$user = JFactory::getUser();
		if (!$user->authorise('core.edit', 'com_plugins'))
			return;

		$app->setHeader('Content-disposition', 'attachment; filename="n3t-cookie-consent.json"', true);
		$app->setHeader('Content-Type', 'application/json', true);

		echo $this->params->toString();
	}

	/**
	 * Joomla Ajax Event
	 * index.php?option=com_ajax&group=system&format=raw&plugin=N3tCookieConsentImport
	 * Import plugin settings in JSON format. Supported formats
	 * - export from plugin settings
	 * - export from CookieBot.com scan
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onAjaxN3tCookieConsentImport()
	{
		$app = Factory::getApplication();
		if (!$app->isClient('administrator'))
			return;

		$user = JFactory::getUser();
		if (!$user->authorise('core.edit', 'com_plugins'))
			return;

		$this->loadLanguage();
		$plugin = PluginHelper::getPlugin($this->_type, $this->_name);

		$files = $this->getInput()->files->get('n3tcc');
		if (!is_array($files) || !isset($files['import']) || $files['import']['error']) {
			$app->enqueueMessage(Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_CFG_IMPORT_ERROR_FILE'));
			$app->redirect(Route::_('index.php?option=com_plugins&view=plugin&layout=edit&extension_id=' . $plugin->id, false));
			return;
		}

		// TODO detekce chyby
		$params = new Joomla\Registry\Registry(file_get_contents($files['import']['tmp_name']));

		if ($params->get('privacy_policy_type')) {
			// Plugin export
			$this->params = $params;

			$this->updateParams();
			$app->enqueueMessage(Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_CFG_IMPORT_SUCCESS'));
		} elseif ($params->get('domain') && $params->get('cookies')) {
			// CookieBot.com export
			$blocks = [];
			$blocks[0] = [
				'type' => self::BLOCK_DESCRIPTION,
			];
			foreach ($params->get('cookies') as $cookie) {
				if (!isset($blocks[(int)$cookie->Category])) {
					$blocks[(int)$cookie->Category] = [
						'type' => ['', self::BLOCK_FUNCTIONAL, self::BLOCK_PREFERENCES, self::BLOCK_ANALYTICS, self::BLOCK_MARKETING, self::BLOCK_UNKNOWN][(int)$cookie->Category],
						'cookies' => [],
					];
				}

				$this->parseExpiration($cookie->ExpireDescription, $expiration, $expirationUnit);
				$blockCookie = [
					'name' => $cookie->NamePattern ?: $cookie->Name,
					'description' => $cookie->PurposeDescription,
					'regex' => (int)!!$cookie->NamePattern,
					'provider' => $cookie->Provider,
					'expiration' => $expiration,
					'expiration_unit' => $expirationUnit,
				];

				$blocks[(int)$cookie->Category]['cookies'][] = $blockCookie;
			}

			$blocks[1000] = [
				'type' => self::BLOCK_PRIVACY,
			];

			$blocks[1001] = [
				'type' => self::BLOCK_CONSENT,
			];

			$this->params->set('blocks', $blocks);

			$this->updateParams();
			$app->enqueueMessage(Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_CFG_IMPORT_SUCCESS_COOKIEBOT', $params->get('domain')));
		} else {

			$app->enqueueMessage(Text::_('PLG_SYSTEM_N3TCOOKIECONSENT_CFG_IMPORT_ERROR_FORMAT'));
		}

		$app->redirect(Route::_('index.php?option=com_plugins&view=plugin&layout=edit&extension_id=' . $plugin->id, false));
	}

	/**
	 * Joomla Ajax Event
	 * index.php?option=com_ajax&group=system&format=raw&plugin=N3tCookieConsentLogConsent
	 * Log user consent
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onAjaxN3tCookieConsentLogConsent()
	{
		if ($this->params->get('log_consents', 1)) {
			$this->loadLanguage();

			Log::addLogger(
				[
					'text_file'         => $this->logFile(),
					'text_entry_format' => "{DATETIME}\t{CLIENTIP}\t{MESSAGE}"
				],
				JLog::ALL,
				['n3t_cookie_consent']
			);

			$consent    = $this->getConsent();
			$categories = implode(',', $consent->get('level', []));
			$guid       = $consent->get('data.guid');
			Log::add(Text::sprintf('PLG_SYSTEM_N3TCOOKIECONSENT_LOG_CONSENT', $categories, (int) $this->params->get('revision'), $guid), Log::INFO, 'n3t_cookie_consent');
		}

		// Clear unallowed cookies
		$input = $this->getInput();

		$appConfig = JFactory::getConfig();
		$path = $appConfig->get('cookie_path', '/');
		$domain = $appConfig->get('cookie_domain');

		foreach($input->cookie->getArray() as $cookieName => $value) {
			if (!$this->isCookieEnabled($cookieName)) {
				$this->debuggerBlockedCookies[] = $cookieName;
				$input->cookie->set($cookieName, '', 1, $path, $domain);
			}
		}
	}

	/**
	 * Joomla Ajax Event
	 * index.php?option=com_ajax&group=system&format=raw&plugin=N3tCookieConsentDefaults
	 * Scan frontpage on Cookies
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onAjaxN3tCookieConsentDefaults()
	{
		$this->loadLanguage();

		$app = Factory::getApplication();
		if (!$app->isClient('administrator'))
			return;

		$user = JFactory::getUser();
		if (!$user->authorise('core.edit', 'com_plugins'))
			return;

		$blocks = [];
		$blocks[] = (object)['type' => self::BLOCK_DESCRIPTION];
		$blocks[] = (object)['type' => self::BLOCK_FUNCTIONAL];
		$blocks[] = (object)['type' => self::BLOCK_PREFERENCES];
		$blocks[] = (object)['type' => self::BLOCK_ANALYTICS];
		$blocks[] = (object)['type' => self::BLOCK_MARKETING];
		$blocks[] = (object)['type' => self::BLOCK_UNKNOWN];
		$blocks[] = (object)['type' => self::BLOCK_HIDDEN];
		$blocks[] = (object)['type' => self::BLOCK_SYSTEM];
		$blocks[] = (object)['type' => self::BLOCK_PRIVACY];
		$blocks[] = (object)['type' => self::BLOCK_CONSENT];
		$this->params->set('blocks', $blocks);
		$this->updateParams();

		// Register basic cookies
		$this->registerUnknownCookie($this->params->get('cookie_name', 'n3t_cc'));
		if ($this->params->get('use_iframe_manager', false))
			$this->registerUnknownCookie($this->params->get('cookie_name', 'n3t_cc') . '_ifm_');
		$this->registerUnknownCookie(md5(ApplicationHelper::getHash('site')));
		$this->registerUnknownCookie(md5(ApplicationHelper::getHash('administrator')));

		$this->registerUnknownCookie('joomla_user_state');
		$this->registerUnknownCookie(ApplicationHelper::getHash('PlgSystemLogout'));
		if (PluginHelper::isEnabled('system', 'remember'))
			$this->registerUnknownCookie('joomla_remember_me_');

		if (Multilanguage::isEnabled())
			$this->registerUnknownCookie('language');

		// Register some core extensions
		if (PluginHelper::isEnabled('captcha', 'recaptcha'))
			$this->registerUnknownCookie('_GRECAPTCHA');
		if (PluginHelper::isEnabled('captcha', 'recaptcha_invisible'))
			$this->registerUnknownCookie('_GRECAPTCHA');

		// Register some other known extensions
		if (PluginHelper::isEnabled('captcha', 'n3tmulticaptcha'))
			$this->registerUnknownCookie(ApplicationHelper::getHash('n3t_multicaptcha'));
		if (PluginHelper::isEnabled('captcha', 'n3tmulticaptcha'))
			$this->registerUnknownCookie(ApplicationHelper::getHash('n3t_multicaptcha'));

		$this->updateParams();

		$plugin = PluginHelper::getPlugin($this->_type, $this->_name);
		$app->redirect(Route::_('index.php?option=com_plugins&view=plugin&layout=edit&extension_id=' . $plugin->id, false));
	}

	/**
	 * Joomla onContentPrepareForm Event
	 * @param $form
	 * @param $data
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onContentPrepareForm($form, $data)
	{
		if (!($form instanceof Form))
			return;
		if ($form->getName() != 'com_plugins.plugin')
			return;
		if (!is_object($data))
			return;
		if (!isset($data->extension_id))
			return;
		$plugin = PluginHelper::getPlugin($this->_type, $this->_name);
		if ($data->extension_id != $plugin->id)
			return;

		$blocksField = $form->getField('blocks', 'params');
		$subform = new SimpleXMLElement($blocksField->formsource);

		if (!$this->params->get('show_cookie_provider', 1)) {
			$node = $subform->xpath(".//field[@name='provider']");
			unset($node[0][0]);
		}

		if (!$this->params->get('show_cookie_expiration', 1)) {
			$node = $subform->xpath(".//field[@name='expiration']");
			unset($node[0][0]);
		}

		if (!$this->params->get('show_cookie_expiration', 1) || !$this->isMultiLanguage) {
			$node = $subform->xpath(".//field[@name='expiration_unit']");
			unset($node[0][0]);
		}

		$form->setFieldAttribute('blocks', 'formsource', $subform->asXML(), 'params');
	}

	/**
	 * Joomla Ajax Event
	 * index.php?option=com_ajax&group=system&format=raw&plugin=N3tCookieConsentSetCookie
	 * Sets cookie value
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onAjaxN3tCookieConsentSetCookie()
	{
		if (version_compare(phpversion(), '7.3.0', '<'))
			return;

		if (!$this->params->get('cookie_domains'))
			return;

		$app = Factory::getApplication();
		$appConfig = JFactory::getConfig();
		$value = $this->getInput()->get('val', '', 'string');
		if (!$value)
			return;
		if (null === json_decode($value))
			return;

		$time = time() + (int)$this->params->get('cookie_expiration', '395') * 24 * 60 * 60;
		$name = $this->params->get('cookie_name', 'n3t_cc');
		$path = $this->params->get('cookie_path', $appConfig->get('cookie_path', '/'));
		$domain = $this->params->get('cookie_domain', $appConfig->get('cookie_domain'));
		if (!$domain)
			$domain = '.' . $this->getInput()->server->get('SERVER_NAME');

		setrawcookie($name, rawurlencode($value), [
			'expires' => $time,
			'path' => $path,
			'domain' => $domain,
			'secure' => $app->isSSLConnection(),
			'httponly' => false,
			'samesite' => 'None',
		]);

		$app->setHeader('Content-Type', 'image/svg+xml', true);
		return '<svg xmlns="http://www.w3.org/2000/svg"/>';
	}

	/**
	 * Joomla Ajax Event
	 * index.php?option=com_ajax&group=system&format=raw&plugin=N3tCookieConsentExportLog
	 * Export consents log
	 *
	 * @throws Exception
	 * @since 4.0.0
	 */
	public function onAjaxN3tCookieConsentExportLog()
	{
		$app = Factory::getApplication();
		if (!$app->isClient('administrator'))
			return;

		$user = JFactory::getUser();
		if (!$user->authorise('core.edit', 'com_plugins'))
			return;

		$logFile = Factory::getApplication()->get('log_path', JPATH_ADMINISTRATOR . '/logs') . '/' . $this->logFile();

		header('Content-disposition:  attachment; filename="n3t-cookie-consent.log"', true);
		header('Content-Type: text/plain; charset=UTF-8', true);
		if (!file_exists($logFile)) {
			header('Content-Length: 0', true);
			return;
		}

		$size = filesize($logFile);
		header('Content-Length: ' . $size, true);
		readfile($logFile);
	}

	/**
	 * Renders Joomla Layout file with correct paths
	 *
	 * @param string $layoutFile
	 * @param array $displayData
	 *
	 * @since 4.1.5
	 */
	private function renderLayout(string $layoutFile, array $displayData = []): string {
		ob_start();
		require PluginHelper::getLayoutPath($this->_type, $this->_name, $layoutFile);
		return ob_get_clean();
	}
}
