<?php
/**
 * @version    $Id$
 * @package    JSN_PageBuilder_4
 * @author     JoomlaShine Team <support@joomlashine.com>
 * @copyright  Copyright (C) 2012 JoomlaShine.com. All Rights Reserved.
 * @license    GNU/GPL v2 or later http://www.gnu.org/licenses/gpl-2.0.html
 *
 * Websites: http://www.joomlashine.com
 * Technical Support:  Feedback - http://www.joomlashine.com/contact-us/get-support.html
 */

// No direct access to this file.
defined('_JEXEC') or die('Restricted access');

// Register necessary helper classes.
JLoader::register(
	'JSNPageBuilder4Helper',
	JPATH_ADMINISTRATOR . '/components/com_pagebuilder4/helpers/pagebuilder4.php'
);

/**
 * System plugin of JSN PageBuilder 4.
 *
 * @package  JSN_PageBuilder_4
 * @since    1.0.0
 */
class plgSystemPageBuilder4 extends JPlugin
{
	/**
	 * Define signatures to detect content generated by JSN PageBuidler 4.
	 */
	public static $signPattern = '/<!-- Start_PF_Hash\|([^\|]+)\|End_PF_Hash -->/';
	public static $startHtml = '<!-- Start JSN PageBuidler 4 -->';
	public static $endHtml = '<!-- End JSN PageBuidler 4 -->';

	/**
	 * Affects constructor behavior. If true, language files will be loaded automatically.
	 *
	 * @var  boolean
	 */
	protected $autoloadLanguage = true;

	/**
	 * Joomla application object.
	 *
	 * @var  JApplicationCms
	 */
	protected $app;

	/**
	 * Joomla database object.
	 *
	 * @var  JDatabaseDriver
	 */
	protected $dbo;

	/**
	 * Joomla session object.
	 *
	 * @var  JSessionStorage
	 */
	protected $session;

	/**
	 * Current Joomla user.
	 *
	 * @var  JUser
	 */
	protected $user;

	/**
	 * JSN PageBuilder 4 config object.
	 *
	 * @var  array
	 */
	protected $config;

	/**
	 * Requested component.
	 *
	 * @var  string
	 */
	protected $option;

	/**
	 * Requested task.
	 *
	 * @var  string
	 */
	protected $task;

	/**
	 * Requested view.
	 *
	 * @var  string
	 */
	protected $view;

	/**
	 * Requested layout.
	 *
	 * @var  string
	 */
	protected $layout;

	/**
	 * Requested response template.
	 *
	 * @var  string
	 */
	protected $tmpl;

	/**
	 * Requested editor.
	 *
	 * @var  string
	 */
	protected $editor;

	/**
	 * Is an admin page requested?
	 *
	 * @var  boolean
	 */
	protected $isAdmin;

	/**
	 * Requested menu item ID.
	 *
	 * @var  integer
	 */
	protected $itemID;

	/**
	 * Response HTML produced by Joomla.
	 *
	 * @var  string
	 */
	protected $response;

	/**
	 * Flag to determine whether frontend stylesheets should be optimized.
	 *
	 * @var  boolean
	 */
	protected $optimizeFrontendAssets = false;

	/**
	 * Constructor.
	 *
	 * @param   object  &$subject  The object to observe
	 * @param   array   $option    An optional associative array of configuration settings.
	 *                             Recognized key values include 'name', 'group', 'params', 'language'
	 *                             (this list is not meant to be comprehensive).
	 *
	 * @return  void
	 */
	public function __construct($subject, $option = array())
	{
		parent::__construct($subject, $option);

		// Get Joomla application object.
		$this->app = JFactory::getApplication();

		// Get Joomla database object.
		$this->dbo = JFactory::getDbo();

		// Get Joomla session object.
		$this->session = JFactory::getSession();

		// Get the current Joomla user.
		$this->user = JFactory::getUser();

		// Check if an admin page is requested?
		$this->isAdmin = method_exists($this->app, 'isAdmin')
			? $this->app->isAdmin()
			: $this->app->isClient('administrator');
	}

	/**
	 * Initialize JSN PageBuilder 4.
	 *
	 * @return  void
	 */
	public function onAfterInitialise()
	{
		// Make sure the JSN PageBuilder 4 component is installed.
		if (!class_exists('JSNPageBuilder4Helper'))
		{
			return;
		}

		// If Joomla site is offline and preview link is requested, temporarily make site online.
		$config = JFactory::getConfig();

		if ((int) $config->get('offline')
			&& $this->app->input->getInt('pb4preview')
			&& $this->app->input->getString('token') === md5($config->get('secret')))
		{
			$config->set('offline', 0);
		}

		// Get JSN PageBuilder 4 config.
		$this->config = JSNPageBuilder4Helper::getConfig();

		// Load additional plugins for JSN PageBuilder 4.
		JPluginHelper::importPlugin('pagebuilder4');

		// Register onAfterRoute event handler.
		$this->app->registerEvent('onAfterRoute', array(&$this, 'onAfterRoute'));
	}

	/**
	 * Listen to onAfterRoute event.
	 *
	 * @return  void
	 */
	public function onAfterRoute()
	{
		// Make sure this event handler is executed at last order.
		if (!isset($this->onAfterRouteReordered))
		{
			$this->onAfterRouteReordered = true;

			return;
		}

		// Get request variables.
		$this->option = $this->app->input->getCmd('option');
		$this->task = $this->app->input->getCmd('task');
		$this->view = $this->app->input->getCmd('view');
		$this->layout = $this->app->input->getCmd('layout');
		$this->tmpl = $this->app->input->getCmd('tmpl');
		$this->editor = $this->app->input->getCmd('editor');

		// Get the menu item ID of the current page if necessary.
		if (class_exists('JsnExtFwHelper') && JsnExtFwHelper::isSite())
		{
			if ($this->app->input->exists('Itemid'))
			{
				$this->itemID = $this->app->input->getInt('Itemid');
			}
			else
			{
				$menu = $this->app->getMenu('site');

				if (!( $item = $menu->getActive() ))
				{
					$lang = JFactory::getLanguage();

					if (JLanguageMultilang::isEnabled())
					{
						$item = $menu->getDefault($lang->getTag());
					}
					else
					{
						$item = $menu->getDefault();
					}
				}

				$this->itemID = $item->id;
			}
		}

		// Override the default Joomla editor with JSN PageBuilder 4 if necessary.
		if ($this->editor === 'pagebuilder4' && JSNPageBuilder4Helper::isComponentSupported($this->option))
		{
			// Get Joomla global config.
			$config = JFactory::getConfig();

			// Backup the original editor.
			define('ORIGINAL_EDITOR', $this->user->getParam('editor', $config->get('editor')));

			// Override the original editor.
			$this->user->setParam('editor', 'pagebuilder4');
			$config->set('editor', 'pagebuilder4');
		}

		// Register onBeforeRender event handler.
		$this->app->registerEvent('onBeforeRender', array(&$this, 'onBeforeRender'));

		if (class_exists('JsnExtFwHelper') && JsnExtFwHelper::isAdmin())
		{
            // Check if if module built by JSN PageBuilder 4 then showing a notice.
		    $this->checkModuleBeforeDuplicate();
		}
	}

	/**
	 * Listen to onBeforeRender event.
	 *
	 * @return  void
	 */
	public function onBeforeRender()
	{
		// Make sure this event handler is executed at last order.
		if (!isset($this->onBeforeRenderReordered))
		{
			$this->onBeforeRenderReordered = true;

			return;
		}

		// Simply return if required class is missing.
		if (!class_exists('JsnExtFwAssets'))
		{
			return;
		}

		// Check if either JSN PageBuilder 4 component is requested
		// or JSN PageBuilder 4 editor plugin is in use.
		if ($this->editor === 'pagebuilder4'
			|| $this->option === 'com_pagebuilder4'
			|| ($this->isAdmin && JSNPageBuilder4Helper::isComponentSupported($this->option)))
		{
			// Init Google Analytics.
			//JSNPageBuilder4Helper::initEventTracking();

			// Init edition manager.
			JSNPageBuilder4Helper::initEditionManager();

			// Init editor data if necessary.
			if ($this->option !== 'com_pagebuilder4')
			{
				JSNPageBuilder4Helper::initEditorData();
			}

			// Load helper script if necessary.
			if ($this->isAdmin && JSNPageBuilder4Helper::isComponentSupported($this->option))
			{
				JsnExtFwAssets::loadScript(
					JUri::root() . 'plugins/editors/pagebuilder4/assets/app/pagefly/core/helper.js'
				);
			}
		}

		// Register onAfterRender event handler.
		//$this->app->registerEvent('onAfterRender', array(&$this, 'onAfterRender'));
	}

	/**
	 * Listen to onAfterRender event.
	 *
	 * @return  void
	 */
	public function onAfterRender()
	{
		// Make sure this event handler is executed at last order.
		/*if (!isset($this->onAfterRenderReordered))
		{
			$this->onAfterRenderReordered = true;

			return;
		}*/

		// Get response body.
		$this->response = $this->app->getBody();

		// Remove JSN PageBuilder 4 from the editor list on Global Configuration and User Profile screen.
		if ($this->option === 'com_config'
			|| (in_array($this->option, array('com_admin', 'com_users')) && $this->layout === 'edit'))
		{
			JFactory::getLanguage()->load('plg_editors_pagebuilder4', JPATH_ADMINISTRATOR);

			if (preg_match(
				'#<option[^>]+>\s*' . JText::_('PLG_EDITORS_PAGEBUILDER4') . '\s*</option>#',
				$this->response,
				$match
			))
			{
				$this->response = str_replace($match[0], '', $this->response);
			}
		}

		if (!$this->isAdmin)
		{
			// Render dynamic content embedded in JSN PageBuilder 4 content.
			$this->renderPageBuilderContent($this->response);

			// Optimize frontend stylesheets if flag set.
			if ($this->optimizeFrontendAssets)
			{
				$this->optimizePageBuilderAssets($this->response);
			}
		}

		// Set response body.
		$this->app->setBody($this->response);

		/* Register onAfterRespond event handler.
		$this->app->registerEvent('onAfterRespond', array( &$this, 'onAfterRespond'));
	}

	/**
	 * Listen to onAfterRespond event.
	 *
	 * @return  void
	public function onAfterRespond()
	{
		// Make sure this event handler is executed at last order.
		if (!isset($this->onAfterRespondReordered))
		{
			$this->onAfterRespondReordered = true;

			return;
		}

		// Capture output.
		$contents = ob_get_contents();

		ob_end_clean();

		echo $contents;
	 */
	}

	/**
	 * Listen to onContentAfterSave event.
	 *
	 * @param   string   $context  The current context.
	 * @param   JTable   $table    Current database table object.
	 * @param   boolean  $isNew    Whether saving a new content item.
	 * @param   array    $data     Current content item data.
	 *
	 * @return  void
	 */
	public function onContentAfterSave($context, $table, $isNew, $data = null)
	{
		if ($isNew && $this->editor !== 'pagebuilder4' && $this->app->input->getCmd('task') === 'save2copy')
		{
			// Check if content is built with JSN PageBuilder 4.
			$contentKey = null;
			$currentHash = null;

			foreach ($table->getProperties() as $k => $v)
			{
				if (preg_match(self::$signPattern, $v, $match))
				{
					$contentKey = $k;
					$currentHash = $match[1];

					break;
				}
			}

			if ($contentKey && $currentHash)
			{
				// Generate new page hash.
				$newHash = JSNPageBuilder4Helper::generatePageHash();

				// Get current page data.
				$page = JSNPageBuilder4Helper::getPageData($currentHash);

				// Save new page data.
				JSNPageBuilder4Helper::savePageData(
					$this->app->input->getCmd('option'),
					$newHash,
					$page['data'],
					$table->{$contentKey}
				);

				// Replace current hash with new hash in content.
				$table->{$contentKey} = str_replace($currentHash, $newHash, $table->{$contentKey});
				$table->store();
			}
		}
	}

	/**
	 * Listen to onContentChangeState event.
	 *
	 * @param   string   $context  The current context.
	 * @param   integer  $ids      An array of item IDs that state are changed.
	 * @param   integer  $state    The new item state.
	 *
	 * @return  boolean
	 */
	public function onContentChangeState($context, $ids, $state)
	{
		// Check if there is any plugin is being unpublished.
		if ($context === 'com_plugins.plugin' && $state == 0)
		{
			foreach ($ids as $id)
			{
				// Get plugin details.
				$plugin = $this->dbo->setQuery(
					"SELECT * FROM #__extensions WHERE extension_id = {$id}"
				)->loadObject();

				// Prevent unpublishing all JSN PageBuilder 4 plugins.
				$is_core = $plugin->element === 'pagebuilder4'
					&& in_array($plugin->folder, array('editors', 'editors-xtd', 'system'));

				$is_integration = $plugin->folder === 'pagebuilder4' && $plugin->element === 'integration';

				if ($is_core || $is_integration)
				{
					$this->dbo->setQuery(
						"UPDATE #__extensions SET enabled = 1 WHERE extension_id = {$id}"
					)->execute();

					// Load necessary language files.
					JFactory::getLanguage()->load('plg_system_pagebuilder4', JPATH_ADMINISTRATOR);

					// Set a message to let the user know that the plugin is required by JSN PageBuilder 4.
					$this->app->enqueueMessage(
						JText::sprintf(
							'JSN_PAGEBUILDER4_CANNOT_UNPUBLISH_A_REQUIRED_PLUGIN', JText::_($plugin->name)
						),
						'info'
					);

					return false;
				}
			}
		}
	}

	/**
	 * Listen to onContentAfterDelete event.
	 *
	 * @param   string  $context  The current context.
	 * @param   JTable  $table    The current table data.
	 *
	 * @return  void
	 */
	public function onContentAfterDelete($context, $table)
	{
		// Check if the item being removed is created by JSN PageBuilder 4.
		$hashes = array();

		foreach ($table->getProperties() as $value)
		{
			if (gettype($value) !== 'string') continue;
			
			if (preg_match_all(self::$signPattern, $value, $matches, PREG_SET_ORDER))
			{
				foreach ($matches as $match)
				{
					$hashes[] = $match[1];
				}
			}
		}

		// Remove all JSN PageBuilder 4 data associated with the item being removed.
		if (count($hashes))
		{
			$where = 'hash IN (' . implode(', ', array_map(array($this->dbo, 'quote'), $hashes)) . ')';

			// Remove all page data.
			$this->dbo->setQuery("DELETE FROM #__jsn_pagebuilder4_pages WHERE {$where}")->execute();

			// Remove all page revisions.
			$this->dbo->setQuery("DELETE FROM #__jsn_pagebuilder4_revisions WHERE {$where}")->execute();
		}
	}

	/**
	 * Listen to onExtensionBeforeSave event.
	 *
	 * @param   string   $context  The current context.
	 * @param   JTable   $table    The current table data.
	 * @param   boolean  $new      Whether this is a new item?
	 *
	 * @return  boolean
	 */
	public function onExtensionBeforeSave($context, $table, $new)
	{
		// Check if a plugin required JSN PageBuilder 4 is being unpublished.
		if ($context === 'com_plugins.plugin' && $table->enabled == 0)
		{
			$is_core = $table->element === 'pagebuilder4'
				&& in_array($table->folder, array('editors', 'editors-xtd', 'system'));

			$is_integration = $table->folder === 'pagebuilder4' && $table->element === 'integration';

			if ($is_core || $is_integration)
			{
				// Load necessary language files.
				JFactory::getLanguage()->load('plg_system_pagebuilder4', JPATH_ADMINISTRATOR);

				// Set a message to let the user know that this plugin is required.
				$table->setError(
					JText::sprintf('JSN_PAGEBUILDER4_CANNOT_UNPUBLISH_A_REQUIRED_PLUGIN', JText::_($table->name)),
					'warning'
				);

				return false;
			}
		}
	}

	/**
	 * Listen to onExtensionAfterSave event.
	 *
	 * @param   string   $context  The current context.
	 * @param   JTable   $table    Current database table object.
	 * @param   boolean  $isNew    Whether saving a new content item.
	 *
	 * @return  void
	 */
	public function onExtensionAfterSave($context, $table, $isNew)
	{
		$this->onContentAfterSave($context, $table, $isNew);
	}

	/**
	 * Listen to onExtensionAfterDelete event.
	 *
	 * @param   string  $context  The current context.
	 * @param   JTable  $table    The current table data.
	 *
	 * @return  void
	 */
	public function onExtensionAfterDelete($context, $table)
	{
		$this->onContentAfterDelete($context, $table);
	}

	/**
	 * Handle Ajax requests.
	 *
	 * @return  void
	 */
	public function onAjaxPagebuilder4()
	{
		// Disable all error reporting to prevent unwanted side-effect when producing response.
		error_reporting(0);

		// Because we don't need token on public get action, let's emulate one.
		if (strtoupper($_SERVER['REQUEST_METHOD']) === 'GET'
			&& strpos($this->app->input->getCmd('action'), 'get-') === 0)
		{
			$this->app->input->get->set(JSession::getFormToken(), 1);
		}

		// Set necessary headers.
		header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
		header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
		header("Cache-Control: no-store, no-cache, must-revalidate");
		header("Cache-Control: post-check=0, pre-check=0", false);
		header("Pragma: no-cache");

		// Register class to handle Ajax request.
		JLoader::register(
			'JsnExtFwAjaxPb4Editor', dirname(__FILE__) . '/includes/ajax/pb4-editor.php'
		);

		// Set necessary context.
		$this->app->input->set('context', 'pb4-editor');

		// Let JSN Ext. Framework G2 handle Ajax request.
		JsnExtFwAjax::execute();

		// Exit immediately to prevent Joomla from processing further.
		exit();
	}

	/**
	 * Register custom input controls.
	 *
	 * @param   array  &$paths  Array of path to look for custom input controls
	 *
	 * @return  void
	 */
	public function onJsnExtFwGetInputControlPath(&$paths)
	{
		$path = dirname(__FILE__) . '/assets/js/inputs';
		$paths[$path] = JUri::root(true) . '/plugins/system/pagebuilder4/assets/js/inputs';
	}

	/**
	 * Register additional stylesheet on selector screen.
	 *
	 * @param   string  $context       Current selector screen's context.
	 * @param   array   &$stylesheets  Current additional stylesheets to load on selector screen.
	 * @param   array   &$scripts      Current additional script files to load on selector screen.
	 *
	 * @return  void
	 */
	public function onJsnExtFwGetSelectorScreenAssets($context, &$stylesheets, &$scripts)
	{
		$stylesheets[] = JUri::root() . 'plugins/system/pagebuilder4/assets/css/selector.css';
	}

	/**
	 * Method to render dynamic contents embedded in JSN PageBuilder 4 outputs.
	 *
	 * @param   string  &$output  Current HTML output.
	 *
	 * @return  void
	 */
	protected function renderPageBuilderContent(&$output)
	{
		// Check if the current HTML output contains JSN PageBuidler 4 outputs.
		if (preg_match_all(self::$signPattern, $output, $signs, PREG_SET_ORDER))
		{
			// Set flag to optimize the loading of frontend stylesheets later.
			if (!$this->optimizeFrontendAssets)
			{
				$this->optimizeFrontendAssets = true;
			}

			// Loop thru matched signs to get JSN PageBuidler 4 outputs and render dynamic contents.
			foreach ($signs as $sign)
			{
				// Store page hash for later reference if needed.
				$hash = $sign[1];

				// Split current output by matched sign.
				$parts = explode($sign[0], $output);

				// Loop thru all parts of output to get JSN PageBuidler 4 outputs and render dynamic contents.
				foreach ($parts as &$part)
				{
					// Simply continue of current part does not contain JSN PageBuidler 4 output.
					if (strpos($part, self::$startHtml) === false && strpos($part, self::$endHtml) === false)
					{
						continue;
					}

					// Get JSN PageBuidler 4 output from current part.
					@ list($pre, $post) = explode(self::$startHtml, $part, 2);
					@ list($content, $post) = explode(self::$endHtml, $post, 2);

					// Trigger an event to render dynamic content.
					$this->app->triggerEvent('onPageBuilder4RenderDynamicContent', array(&$content));

					// Update current part.
					$part = $pre . $content . $post;
				}

				$output = implode($parts);
			}

			//convert HTMLT Entities To Character In Table Element
			//$output = $this->convertHTMLTEntitiesToCharacterInTableElement($output);

			// Recursively render dynamic content.
			$this->renderPageBuilderContent($output);
		}
	}

	/**
	 * Method to convert HTMLT Entities To Character In Table Element
	 *
	 * @param   string  $output  Current HTML output.
	 *
	 * @return  string
	 */

	protected function convertHTMLTEntitiesToCharacterInTableElement($output)
	{
		preg_match_all('#<(td|th) data-cell-id="[^"](.*?)[^"]" class="[^"](.*?)[^"]">(.*?)</(td|th)>#si', $output, $matches, PREG_SET_ORDER);

		if (count($matches))
		{
			foreach ($matches as $key => $value) {
				if (isset($value[4]))
				{
					$tmp = html_entity_decode($value[4], ENT_COMPAT, 'UTF-8');
					$output = str_replace($value[4], $tmp, $output);
				}
			}
		}

		return $output;
	}

	/**
	 * Handle event onPageBuilder4RenderDynamicContent.
	 *
	 * @param   string  $html  Currently generated HTML output.
	 *
	 * @return  void
	 */
	public function onPageBuilder4RenderDynamicContent(&$html)
	{
		// Replace base URL.
		$this->replaceBaseUrl($html);

		// Render syntax for embedding dynamic content.
		$this->renderDynamicContent($html);

		// Render syntax for embedding Google reCaptcha for contact form.
		$this->renderGoogleRecaptcha($html);

		// Adds on the fly the loading=lazy attribute to image tags
		$this->handleLazyLoad($html);		
	}

	/**
	 * Method to replace all {{base_url}} placeholder with real site URL.
	 *
	 * @param   string  &html  Currently generated HTML code.
	 *
	 * @return  void
	 */
	protected function replaceBaseUrl(&$html)
	{
		// Refine all {{base_url}} placeholders.
		$html = str_replace(array('%7B%7Bbase_url%7D%7D', '%7B{base_url}%7D'),'{{base_url}}', $html);
		$html = str_replace(array(JUri::root() . '{{', JUri::root(true) . '/{{'), '{{', $html);

		// Replace all {{base_url}} placeholders with real site URL.
		$pattern = '#(["\'(=]){{base_url}}([^)\'"}]+)([)\'"}])#';
		$base = JUri::root();

		if (preg_match_all($pattern, $html, $matches, PREG_SET_ORDER))
		{
			foreach ($matches as $match)
			{
				// Refine link.
				$link = str_replace('&amp;', '&', $match[2]);

				if (strpos($link, 'index.php') !== false)
				{
					// Trigger an event to get SEF link.
					$this->app->triggerEvent('onPageBuilder4GetSefLink', array(&$link));
				}

				if (substr($link, 0, 1) === '/')
				{
					$link = JUri::getInstance()->toString(array('scheme', 'user', 'pass', 'host', 'port')) . $link;
				}
				elseif (!preg_match('/^https?:/', $link))
				{
					$link = "{$base}{$link}";
				}

				// Replace the {{base_url}} placeholder in the current link with real site URL.
				$html = str_replace($match[0], "{$match[1]}{$link}{$match[3]}", $html);
			}
		}

    	/**
         * Replace all relative links with absolute URLs in CSS rules.
         */
    	$base = JUri::root();
        if (preg_match_all('/url\(\s*[\'"]*([^\)]+)[\'"]*\s*\)/', $html, $matches, PREG_SET_ORDER)) 
        {
            foreach ($matches as $match) 
            {
                // If link is relative, convert it to absolute.
                $match[1] = trim($match[1], '\'"');
                if (!preg_match('/^([a-zA-Z0-9]+:|\/|#)/', $match[1])) 
                {
                    $newUrl = "{$base}{$match[1]}";
                    $newUrl = 'url(' . $newUrl . ')';
                    $html = str_replace($match[0], $newUrl, $html);
                }
            }
        }		
	}

	/**
	 * Method to render syntax for embedding a list of dynamic content.
	 *
	 * @param   string  &$html  Currently generated HTML code.
	 *
	 * @return  void
	 */
	protected function renderDynamicContent(&$html)
	{
		// Render all <!-- PF_DYNAMIC_CONTENT|...|END_PF_DYNAMIC_CONTENT /--> tags if exists.
		$pattern = '#<!-- PF_DYNAMIC_CONTENT\|\{"([^"]+)": ([^\|]+)\}\|END_PF_DYNAMIC_CONTENT /-->#';

		if (preg_match_all($pattern, $html, $matches, PREG_SET_ORDER))
		{
			// Get data for rendering dynamic content.
			$data = array();

			foreach ($matches as $match)
			{
				// Clear embed tag.
				$html = str_replace($match[0], '', $html);

				// Get element ID and filters.
				$id = $match[1];
				$opts = json_decode($match[2]);

				// Generate filters array.
				$filters = array();

				if (!empty($opts->author))
				{
					$filters['author_id'] = array_filter(explode(',', $opts->author), 'intval');
				}

				if (!empty($opts->category))
				{
					$filters['category_id'] = array_filter(explode(',', $opts->category), 'intval');
					$filters['nested_categories'] = isset($opts->nestedCategories) ? $opts->nestedCategories : 1;
				}

				if (!empty($opts->tag))
				{
					$filters['tag'] = array_filter(explode(',', $opts->tag), 'intval');
				}

				if (!empty($opts->search))
				{
					$filters['search'] = trim($opts->search);
				}

				// Get dynamic content based on element filters.
				$limit = empty($opts->limit) ? 20 : (int) $opts->limit;
				$order = (empty($opts->orderBy) ? '' : $opts->orderBy);
				$order = (empty($order) && empty($opts->orderDirection)) ? '' : "{$order} {$opts->orderDirection}";

				$data["{$opts->listType}>{$id}"] = JSNPageBuilder4Helper::getContentItems(
					$opts->listType, $limit, 0, $order, $filters
				);
			}

			// Render dynamic content.
			foreach ($data as $key => $items) {
				list($type, $id) = explode('>', $key);

				// Find dynamic content templates to replace with real content.
				$begin = '{{#' . $id . '}}';
				$end = '{{/' . $id . '}}';

				if (strpos($html, $begin) !== false && strpos($html, $end) !== false)
				{
					// Get dynamic content templates.
					$parts = explode($begin, $html);
					$matches = array();

					for ($i = 1, $n = count($parts); $i < $n; $i++)
					{
						$matches[] = array(
							'in' => current(explode($end, $parts[$i])),
							'out' => ''
						);
					}

					// Replace template tags with real data.
					$numTemplates = count($matches);

					foreach ($items as $idx => $item)
					{
						// Get dynamic content template to replace with real content.
						if (isset($matches[$idx]))
						{
							$t =& $matches[$idx];
						}
						else
						{
							$t =& $matches[$numTemplates - 1];
						}

						$out = $t['in'];

						// Process conditional logic tags.
						$pattern = '/{{if\s+([^}]+)}}/';

						if (preg_match_all($pattern, $out, $checks, PREG_SET_ORDER))
						{
							for ($i = count($checks) - 1; $i >= 0; $i--)
							{
								$prop = $checks[$i][1];
								$parts = explode($checks[$i][0], $out);

								for ($j = count($parts) - 1; $j > 0; $j--)
								{
									@list($check, $tmp) = explode('{{/if}}', $parts[$j]);
									@list($valid, $backup) = explode('{{else}}', $check);
									$parts[$j] = '';

									if (JSNPageBuilder4Helper::getKeyPathValue($item, $prop))
									{
										$parts[$j] = $valid;
									}
									elseif ($backup)
									{
										$parts[$j] = $backup;
									}

									$parts[$j] .= $tmp;
								}

								$out = implode($parts);
							}
						}

						// Process loop tags.
						$pattern = '/{{loop\s+([^}]+)}}/';

						if (preg_match_all($pattern, $out, $loops, PREG_SET_ORDER))
						{
							for ($i = count($loops) - 1; $i >= 0; $i--)
							{
								$prop = $loops[$i][1];
								$parts = explode($loops[$i][0], $out);

								for ($j = count($parts) - 1; $j > 0; $j--)
								{
									@list($loop, $tmp) = explode('{{/loop}}', $parts[$j]);
									$parts[$j] = array();

									if ($value = JSNPageBuilder4Helper::getKeyPathValue($item, $prop))
									{
										if (is_array($value))
										{
											$value = array_filter($value);

											for ($k = 0, $n = count($value); $k < $n; $k++)
											{
												$parts[$j][] = preg_replace(
													'/{{([^}]+)}}/', "{{\\1.{$k}}}", $loop
												);
											}
										}
										elseif ($value)
										{
											$parts[$j][] = $loop;
										}
									}

									$parts[$j] = implode($parts[$j]) . $tmp;
								}

								$out = implode($parts);
							}
						}

						// Find template tags to replace with real data.
						$pattern = '/{{([^}]+)}}/';

						if (preg_match_all($pattern, $out, $tags, PREG_SET_ORDER))
						{
							// Add support for image tag's alt and title attribute.
							$tags[] = array('{{alt_attr}}', 'alt_attr');
							$tags[] = array('{{title_attr}}', 'title_attr');
							$tags[] = array('{{pb_alt_image_content}}', 'pb_alt_image_content');
							$iAttrs = 'alt="{{pb_alt_image_content}}" title="{{pb_alt_image_content}}"';

							$out = str_replace('alt="{{title}}"', 'alt="{{alt_attr}}"', $out);
							$out = str_replace('title="{{title}}"', 'title="{{title_attr}}"', $out);

							$out = str_replace(
								'<img src="{{image}}">',
								'<img src="{{images}} ' . $iAttrs . '">',
								$out
							);

							$out = str_replace(
								'<img src="{{images.image_intro}}">',
								'<img src="{{images.image_intro}}" ' . $iAttrs . '>',
								$out
							);

							// Loop thru template tags to replace with real data.
							foreach ($tags as $tag)
							{
								// Get tag options.
								$opts = array();

								if (strpos($tag[1], '?') !== false)
								{
									list($tag[1], $params) = explode('?', $tag[1], 2);

									parse_str($params, $opts);
								}

								// Get data to replace.
								$data = JSNPageBuilder4Helper::getKeyPathValue($item, $tag[1]);

								// Refine relative URL.
								if (is_string($data))
								{
									if (strpos($data, '/') !== false && is_file(JPATH_ROOT . "/{$data}"))
									{
										$data = JUri::root() . $data;
									}
								}

								// Process tag options.
								foreach ($opts as $name => $value)
								{
									switch ($name)
									{
										case 'dateFormat':
											$data = date($value, strtotime($data));
										break;

										case 'wordLimit':
											// Clear all plugin tags.
											$data = preg_replace(
												'/({{[^}]*}}|{[^}]*})/',
												' ',
												$data
											);

											// Use method from JSN Ext. Framework 2 to truncate text.
											$data = JsnExtFwText::truncate($data, "{$value}w");
										break;

										case 'fieldId':
										    $tmp = '';

										    if (count($data))
										    {
										        foreach ($data as $prop)
										        {
										            if ($prop['id'] == $value)
										            {
										                $tmp = $prop['value'];

										                break;
										            }
										        }
										    }

										    $data = $tmp;
										    $data = str_replace('$', '&dollar;', $data);
										break;

										case 'fallback':
											if (empty($data))
											{
												$data = $value;
											}
										break;
									}
								}

								// Refine article field data.
								if ($tag[0] === '{{field}}' && !is_string($data))
								{
								    $data = '';
								}

								// Allow 3rd-party to process template tag.
								$this->app->triggerEvent(
									'onPageBuilder4ProcessTemplateTag',
									array($type, $tag, &$data, &$out, $opts, $item)
								);

								// Replace template tag with real data.
								$out = str_replace($tag[0], $data, $out);
							}

							// Update output.
							$t['out'] .= $out;
						}
					}

					// Replace dynamic content templates with real content.
					foreach ($matches as $match)
					{
						$html = preg_replace(
							'/' . preg_quote("{$begin}{$match['in']}{$end}", '/') . '/',
							$match['out'],
							$html,
							1
						);
					}
				}
			}

			// Remove all remaining dynamic content templates.
			if (preg_match('/{{#[a-z0-9]+}}/', $html))
			{
				$parts = preg_split('/{{#[a-z0-9]+}}/', $html);
				$html = $parts[0];

				for ($i = 1, $n = count($parts); $i < $n; $i++)
				{
					$parts[$i] = preg_split('#{{/[a-z0-9]+}}#', $parts[$i]);
					$html .= array_pop($parts[$i]);
				}
			}

			// Remove all empty <script> tags left after processing dynamic content completes.
			$html = str_replace('<script></script>', '', $html);
			$html = str_replace('&dollar;', '$', $html);
		}
	}

	/**
	 * Method to render syntax for embedding Google reCaptcha in a contact form.
	 *
	 * @param   string  &$html  Currently generated HTML code.
	 *
	 * @return  void
	 */
	protected function renderGoogleRecaptcha(&$html)
	{
		// Render all <!-- PF_CONTACT_FORM|...|END_PF_CONTACT_FORM /--> tags if exists.
		$pattern = '#<!-- PF_CONTACT_FORM\|\{"([^"]+)": ([^\|]+)\}\|END_PF_CONTACT_FORM /-->#';

		if (preg_match_all($pattern, $html, $matches, PREG_SET_ORDER))
		{
			// Get Joomla event dispatcher.
			$dispatcher = JEventDispatcher::getInstance();

			// Get Joomla document object.
			$document = JFactory::getDocument();

			foreach ($matches as $match)
			{
				// Parse settings.
				$id = $match[1];
				$cfg = json_decode($match[2]);
				$tag = "<script>{$match[0]}</script>";

				if (isset($cfg->captchaType) && in_array($cfg->captchaType, array('recaptcha', 'invisible_recaptcha')))
				{
					switch ($cfg->captchaType)
					{
						case 'invisible_recaptcha':
							if (class_exists('PlgCaptchaRecaptcha_Invisible')
								|| JPluginHelper::importPlugin('captcha', 'recaptcha_invisible', false))
							{
								// Load the plugin from the database.
								$plugin = JPluginHelper::getPlugin('captcha', 'recaptcha_invisible');

								// Instantiate the plugin.
								$plugin = new PlgCaptchaRecaptcha_Invisible($dispatcher, (array) $plugin);

								// Backup and clear loaded scripts.
								$loaded_script = $document->_script;
								$loaded_scripts = $document->_scripts;
								$document->_scripts = $document->_script = array();

								// Initialize recaptcha.
								$plugin->onInit("pb4_invisible_recaptcha_{$id}");

								// Get assets that need to be loaded.
								$tmp = $this->generateScriptTags($document->_scripts, $document->_script);
								$tmp = str_replace('onload=JoomlaInitReCaptchaInvisible&', '', $tmp);

								// Get HTML tags to render recaptcha.
								$tmp .= $plugin->onDisplay(null, "pb4_invisible_recaptcha_{$id}");
								$tmp .= '<input type="hidden" name="invisible_recaptcha" value="1" />';

								// Restore loaded scripts.
								$document->_script = $loaded_script;
								$document->_scripts = $loaded_scripts;

								// Replace embed tag with real content.
								$html = str_replace($tag, $tmp, $html);
							}
						break;

						case '':
						default:
							if (class_exists('PlgCaptchaRecaptcha')
								|| JPluginHelper::importPlugin('captcha', 'recaptcha', false))
							{
								// Load the plugin from the database.
								$plugin = JPluginHelper::getPlugin('captcha', 'recaptcha');

								// Instantiate the plugin.
								$plugin = new PlgCaptchaRecaptcha($dispatcher, (array) $plugin);

								// Backup and clear loaded scripts.
								$loaded_script = $document->_script;
								$loaded_scripts = $document->_scripts;
								$document->_scripts = $document->_script = array();

								// Initialize recaptcha.
								$plugin->onInit("pb4_recaptcha_{$id}");

								// Get assets that need to be loaded.
								$tmp = $this->generateScriptTags($document->_scripts, $document->_script);
								$tmp = str_replace('onload=JoomlaInitReCaptcha2&', '', $tmp);

								// Get HTML tags to render recaptcha.
								$tmp .= $plugin->onDisplay(null, "pb4_recaptcha_{$id}");
								$tmp .= '<input type="hidden" name="recaptcha" value="1" />';

								// Restore loaded scripts.
								$document->_script = $loaded_script;
								$document->_scripts = $loaded_scripts;

								// Replace embed tag with real content.
								$html = str_replace($tag, $tmp, $html);
							}
						break;
					}
				}
			}
		}
	}

	/**
	 * Method to generate script tags from an array of script declaration.
	 *
	 * @param   array  $scripts
	 * @param   array  $inline_scripts
	 *
	 * @return  string
	 */
	protected function generateScriptTags($scripts, $inline_scripts = array())
	{
		$tags = array();

		foreach ($scripts as $script => $attrs)
		{
			$tags[] = '<script src="'
				. ((isset($attrs['relative']) && $attrs['relative']) ? JUri::root() . '/media/' : '')
				. $script . '"' . ($attrs['type'] ? " type=\"{$attrs['type']}\"" : 'text/javascript')
				. ((isset($attrs['async']) && $attrs['async']) ? ' async="async"' : '')
				. ((isset($attrs['defer']) && $attrs['defer']) ? ' defer="defer"' : '')
				. '></script>';
		}

		foreach ($inline_scripts as $type => $content)
		{
			$tags[] = '<script type="' . ($type ?: 'text/javascript') . '">' . $content . '</script>';
		}

		return implode($tags);
	}

	/**
	 * Method to optimize the loading of frontend assets.
	 *
	 * @param   string  &$output  Currently generated HTML code.
	 *
	 * @return  void
	 */
	protected function optimizePageBuilderAssets(&$output)
	{
		// Make sure flag is set.
		if (!$this->optimizeFrontendAssets)
		{
			return;
		}

		// Optimize the loading of JSN PageBuilder 4 stylesheets.
		$path = '/plugins/editors/pagebuilder4/assets/app/pagefly/';
		$pattern = '#<link[^>]+href="([^"]*' . $path . 'css/main-style.css)(\?[^"]+)*">#';

		$this->optimizeAssets($output, $pattern, 'head');

		// Optimize the loading of JSN PageBuilder 4 helper scripts.
		$pattern = '#<script[^>]+src="([^"]*' . $path . 'core/helper.js)(\?[^"]+)*"[^>]+defer></script>#';

		$this->optimizeAssets($output, $pattern, 'body');

		// Optimize the loading of Google Fonts.
		$this->optimizeGoogleFont($output);

		// Load Custom CSS file
		$customCssFile = JUri::root() . 'plugins/system/pagebuilder4/assets/css/front-end.css';
		$this->loadCustomFiles($customCssFile, $output, 'css');

		// Pass site data to JSN PageBuilder 4 helper script.
		if (strpos($output, 'window.__jsn_pagebuilder4_data__') === false)
		{
			$editorData = '<script>' . JSNPageBuilder4Helper::initEditorData('', false) . '</script>';
			$output = str_replace('</head>', "\t{$editorData}\n</head>", $output);
		}
	}

	/**
	 * Method to optimize the loading of assets.
	 *
	 * @param   string  &$output   HTML content.
	 * @param   string  $pattern   Regular expression pattern to look for tags that load assets.
	 *                             The pattern must capture the URL to the asset file in the first place.
	 * @param   string  $position  Position to load the optimized tags for loading assets, either 'head' or 'body'.
	 *
	 * @return  void
	 */
	protected function optimizeAssets(&$output, $pattern, $position) {
		// Look for all assets loaded in content.
		$assets = array();

		if (preg_match_all($pattern, $output, $matches, PREG_SET_ORDER))
		{
			foreach ($matches as $match)
			{
				// Make sure no stylesheet is loaded more than one time.
				if (!array_key_exists($match[1], $assets))
				{
					$assets[$match[1]] = $match[0];
				}

				$output = str_replace($match[0], '', $output);
			}
		}

		if (count($assets))
		{
			// Load all needed stylesheets in document head.
			$output = str_replace(
				"</{$position}>", "\t" . implode("\n\t", $assets) . "\n</{$position}>", $output
			);
		}
	}

	/**
	 * Method to optimize the loading of Google Fonts.
	 *
	 * @param   string  &$output   HTML content.
	 *
	 * @return  void
	 */
	protected function optimizeGoogleFont(&$output)
	{
		$pattern = '#<link[^>]+href="[^"]*fonts.googleapis.com/css\?family=([^"]+)&display=([^"]+)"[^\/>]+/>#';
		preg_match_all($pattern, $output, $matches, PREG_SET_ORDER);
		$fonts = array();
		if (count($matches))
		{
			foreach ($matches as $match)
			{
				$tmpGFonts = trim($match[1]);
				if ($tmpGFonts != '')
				{
					$explodeTmpGFonts = explode('|', $tmpGFonts);
					if (count($explodeTmpGFonts))
					{
						foreach ($explodeTmpGFonts as $explodeTmpGFont)
						{
							$explodeTmpSingleGFont = explode(':', $explodeTmpGFont);

							if (isset($fonts[$explodeTmpSingleGFont[0]]))
							{
								$tmpExplodeGFont 	= explode(",", $explodeTmpSingleGFont[1]);
								$fontValues 		= explode(",", $fonts[$explodeTmpSingleGFont[0]]);
								$fontValues			= array_merge($fontValues, $tmpExplodeGFont);
								$fontValues 		= array_unique($fontValues);
								$fontValues			= implode(",", $fontValues);
								$fonts [$explodeTmpSingleGFont[0]] = $fontValues;
							}
							else
							{
								$fonts [$explodeTmpSingleGFont[0]] = $explodeTmpSingleGFont[1];
							}
						}
					}
				}
			}
		}

		if (count($fonts))
		{
			$googleFontLink = '';
			foreach ($fonts as $key => $value)
			{
				$googleFontLink .= $key . ':' . $value . '|';
			}

			if ($googleFontLink != '')
			{
				$preloadGoogleFontLink = '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>' . "\n";
				$googleFontLink = '<link href="https://fonts.googleapis.com/css?family=' . rtrim($googleFontLink, '|') . '&display=swap" rel="stylesheet" />';
				$output = preg_replace($pattern, '', $output);
				$output = str_replace(
					"</head>", "\t" . $preloadGoogleFontLink . $googleFontLink . "\n</head>", $output
				);
			}
		}
	}

	/**
	 * Method to add custom files (css, js).
	 *
	 * @param   string  &$body  Currently generated HTML code.
	 * @param   string  $type   The type of custom files.
	 *
	 * @return  void
	 */
	protected function loadCustomFiles($files, &$body, $type)
	{
	    $tmpFiles = array();

	    if ($type == 'css')
	    {
	        $pattern = '<link href="JSN_PB4_CUSTOM_FILE" rel="stylesheet" />';
	    }
	    else
	    {
	        $pattern = '<script src="JSN_PB4_CUSTOM_FILE"></script>';
	    }

	    if (is_array($files))
	    {
	        foreach ($files as $file)
	        {
	            $tmpFiles [] = str_replace("JSN_PB4_CUSTOM_FILE", $file, $pattern);
	        }
	    }
	    else
	    {
	        $tmpFiles [] = str_replace("JSN_PB4_CUSTOM_FILE", $files, $pattern);
	    }

	    if (count($tmpFiles))
	    {
	        $body = str_replace(
	            "</head>", "\t" . implode("\n", $tmpFiles) . "\n</head>", $body
	        );
	    }
	}

	/**
	 * Method to check whether the modules that built JSN PageBuilder 4 or not before duplicating them
	 *
	 * @return  void
	 */
	protected function checkModuleBeforeDuplicate()
	{
	    $task          = $this->app->input->getCmd('task', '');
	    $option        = $this->app->input->getCmd('option', '');
	    $isClient      = $this->app->input->getInt('client_id', 0);
	    $cid           = $this->app->input->post->get('cid', array(), 'array');
	    $moduleTitle   = array();

	    if ($task != 'modules.duplicate' || $isClient || ($option != 'com_modules' && $option != 'com_advancedmodules') || !count($cid))
	    {
	        return;
	    }

	    foreach ($cid as $item)
	    {
	        $isJSNPageBuilder4Content = false;

	        $module = $this->dbo->setQuery(
	            $this->dbo->getQuery(true)
	            ->select('module')
	            ->from('#__modules')
	            ->where('id = ' . $item)
            )->loadResult();

            if ($module != 'mod_custom')
            {
                continue;
            }

            // Create a query object to get content if duplicate an item.
            $result = $this->dbo->setQuery($this->dbo->getQuery(true)
                ->select('id, title, content')
                ->from('#__modules')
                ->where('id = ' . $item)
            )->loadObject();

            if (count((array) $result))
            {
                $isJSNPageBuilder4Content = JSNPageBuilder4Helper::isJSNPageBuilder4Content($result->content);
                if ($isJSNPageBuilder4Content)
                {
                    $moduleTitle [] = $result->title;
                    $index          = array_search($result->id, $cid);

                    if($index !== false)
                    {
                        unset($cid[$index]);
                    }
                }
            }

	    }

	    $this->app->input->post->set('cid', $cid, 'array');
	    if (count($moduleTitle))
	    {
    	    $this->app->enqueueMessage(
    	        JText::sprintf(
    	            'JSN_PAGEBUILDER4_CANNOT_DUPLICATE_MODULES_BUILT_JSN_PAGEBUILDER', implode(' ,', $moduleTitle)),
    	        'Notice'
            );
	    }

	    return;
	}

	/**
	 * Method to loading="lazy" attribute to image tag
	 *
	 * @return  void
	 */
	public function handleLazyLoad(&$html)
	{

		if (class_exists('JsnExtFwHelper'))
		{
			$config = JsnExtFwHelper::getSettings('com_pagebuilder4');
			$browserLevelImageLazyLoading = (boolean) $config['browser_level_image_lazy_loading'];
			if (!$browserLevelImageLazyLoading)
			{
				return;
			}
			
		}
				
		if (strpos($html, '<img') === false)
		{
			return;
		}

		if (!preg_match_all('/<img\s[^>]+>/', $html, $matches))
		{
			return;
		}

		foreach ($matches[0] as $image)
		{
			// Make sure we have a src but no loading attribute
			if (strpos($image, ' src=') !== false && strpos($image, ' loading=') === false)
			{
				$lazyloadImage = str_replace('<img ', '<img loading="lazy" ', $image);
				$html = str_replace($image, $lazyloadImage, $html);
			}
		}
	}	
}
