<?php
/**
 * @package    Essentials for VirtueMart
 * @copyright  Copyright (C) 2012-2024 VirtuePlanet Services LLP. All rights reserved.
 * @license    GNU General Public License version 3 or later; see LICENSE.txt
 * @author     Abhishek Das <info@virtueplanet.com>
 * @link       https://www.virtueplanet.com
 */

defined('_JEXEC') or die;

class VPComDlkHelper extends JObject
{
	protected $input;
	protected $uname;
	protected $dlk;
	protected $data;
	protected $component;
	protected $package;
	protected $params;
	protected $pid;
	protected $time_start;
	protected $time_end;
	protected $adapter;
	protected $processed;
	protected $remote_host;
	protected $remote_host_secured;
	protected $try_count;
	protected $source = 'www.virtueplanet.com';
	protected $updates = null;
	
	protected static $instance = null;
	protected static $encoding = 'base';
	protected static $encoding_suffix = '64';
	
	public function __construct()
	{
		$app              = JFactory::getApplication();
		$encodeFilter     = self::getEncodeFilter();
		
		$input             = $app->input;
		$this->input       = $input;
		$this->component   = $input->get('component', '', 'GET', 'CMD');
		$this->package     = $input->get('package', '', 'GET', 'CMD');
		$options           = new JRegistry(array('follow_location' => true));
		$this->adapter     = JHttpFactory::getAvailableDriver($options);
		$this->remote_host = 'http://validate.virtueplanet.com';
		$this->try_count   = 0;
		
		if (function_exists('extension_loaded') && extension_loaded('openssl'))
		{
			// Secured verification over SSL
			$this->remote_host_secured = 'https://www.virtueplanet.com/validate';
		}
		
		if (!empty($this->component))
		{
			$this->params = JComponentHelper::getParams($this->component);
			$this->pid    = (int) $this->params->get('pid', 0);
			
			if (empty($pid) && !empty($this->component))
			{
				$manifest = JPath::clean(JPATH_ADMINISTRATOR . '/components/' . $this->component . '/vpem.xml');
				
				if (is_file($manifest))
				{
					$xml = simplexml_load_file($manifest);

					if (!empty($xml) && !empty($xml->pid))
					{
						$this->pid = (int) $xml->pid;
					}
				}
			}
		}
		else
		{
			$this->params = new JRegistry;
			$this->pid    = 0;
		}
		
		$this->time_start = microtime(true);
		$this->processed  = 0;
	}

	/**
	* Method to get an instance of the the VPComDlkHelper class
	* 
	* @return object VPComDlkHelper class object
	*/
	public static function getInstance()
	{
		if (self::$instance === null)
		{
			self::$instance = new VPComDlkHelper();
		}
		
		return self::$instance;
	}
	
	public function validate()
	{
		$query = array();
		
		$query['uname']  = $this->input->get('uname', '', 'GET', 'USERNAME');
		$dlk             = $this->input->get('dlk', '', 'GET', 'STRING');
		$query['dlk']    = self::cleanDlk($dlk);
		$query['pid']    = (int) $this->pid;
		$query['host']   = self::encode(self::getHost());
		$query['_pkey']  = md5('time:' . $this->time_start . '.rand:' . mt_rand());

		return $this->_validate($query);
	}
	
	public function revalidate()
	{
		$filter = self::getEncodeFilter();
		$data   = $this->input->get('data', '', 'GET', $filter);
		$data   = self::decodeData($data, true);
		$query  = array();
		
		$query['uname']  = $data['uname'];
		$query['dlk']    = $data['dlk'];
		$query['pid']    = (int) $this->pid;
		$query['host']   = self::encode(self::getHost());
		$query['_pkey']  = md5('time:' . $this->time_start . '.rand:' . mt_rand());
		
		return $this->_validate($query);
	}
	
	public function clear()
	{
		if (!$this->saveParams(''))
		{
			return $this->doReturn(array('error' => true, 'msg' => $this->getMessage()));
		}
		
		return $this->doReturn(array('error' => false, 'msg' => ''));
	}
	
	protected function _validate($query)
	{
		$this->try_count++;
		
		$result = array('error' => true, 'msg' => '', 'return' => null, 'adapter' => '');
		
		if (!JSession::checkToken('get'))
		{
			return $this->doReturn($result);
		}
		
		if (!$this->adapter)
		{
			$result['msg'] = 'JHttpFactory not present. Please upgrade your version of Joomla.';
			return $this->doReturn($result);
		}
		
		$url = $this->remote_host . '/index.php';
		$uri = JUri::getInstance($url);
		$userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'VirtuePlanet Verifier/2.0';
		
		try
		{
			$query_str = http_build_query($query, '', '&');
			$response  = $this->adapter->request('POST', $uri, $query_str, [], 30, $userAgent);
		}
		catch(Exception $e)
		{
			$result['msg'] = $e->getMessage();
			return $this->doReturn($result);
		}
		
		if (empty($response))
		{
			if ($this->try_count == 1 && !empty($this->remote_host_secured))
			{
				// Try again using secured host
				$this->remote_host = $this->remote_host_secured;
				
				return $this->_validate($query);
			}
			
			$result['msg'] = 'Verification failed. No data received.';
			return $this->doReturn($result);
		}
		
		if ($response->code != 200)
		{
			if ($this->try_count == 1 && !empty($this->remote_host_secured))
			{
				// Try again using secured host
				$this->remote_host = $this->remote_host_secured;
				
				return $this->_validate($query);
			}
			
			$result['msg'] = 'Error:' . $response->code . '. Verification failed. Invalid data received.';
			return $this->doReturn($result);
		}

		// Create a shortcut to response body
		$return = $response->body;

		if (empty($return))
		{
			if ($this->try_count == 1 && !empty($this->remote_host_secured))
			{
				// Try again using secured host
				$this->remote_host = $this->remote_host_secured;
				
				return $this->_validate($query);
			}
			
			$result['msg'] = 'Could not fetch data from remote server.';
		}
		else
		{
			if (is_string($return))
			{
				$rawReturn = $return;
				$return = @json_decode($return, true);
				
				if (!is_array($return) || empty($return) || strpos($rawReturn, '.well-known/captcha/') !== false)
				{
					// Could not contact validation server. 
					// We will mock verification success with notification
					$result['error'] = false;
					$host = self::encode(self::getHost());
					
					$return = array();
					$return['access'] = -1;
					$return['valid'] = true;
					$return['message'] = 'Your host server does not allow Joomla to contact our Live Update server.';
				}
				else
				{
					$host   = isset($return['host']) ? $return['host'] : '';
					$_pkey  = isset($return['_pkey']) ? $return['_pkey'] : '';
					
					if ($host != $query['host'])
					{
						$result['msg'] = 'Host name verification failed.';
					}
					elseif ($_pkey != $query['_pkey'])
					{
						$result['msg'] = 'Verification failed.';
					}
					else
					{
						// Verification success
						$result['error'] = false;
					}
				}
				
				$date = JFactory::getDate();
				
				$return['dlk']  = $query['dlk'];
				$return['data'] = self::encode($host . '|*|' . $query['dlk'] . '|*|' . $query['uname'] . '|*|' . intval($return['access']). '|*|' . $date->toSql());
				$return['last_checked'] = JHtml::_('date', $date, 'F d, Y H:i:s');
			}
			else
			{
				$result['msg'] = 'Verification failed. Invalid data format.';
			}
		}

		if (!$result['error'] && !empty($return['dlk']))
		{
			$data = $return['data'];
			
			if (empty($return['valid']))
			{
				$data = '';
			}
			
			if (!$this->saveParams($data))
			{
				$result['error'] = true;
				$result['msg'] = $this->getError();
			}
			
			if (!$result['error'])
			{
				if (!$this->refreshUpdates($return['dlk']))
				{
					$result['error'] = true;
					$result['msg'] = $this->getError();
				}
			}
		}
		else
		{
			$this->saveParams('');
		}
		
		$result['return'] = $return;
		
		return $this->doReturn($result);
	}
	
	public function addDlk(&$url, &$headers)
	{
		$this->processed++;
		
		if (strpos($url, 'https://' . $this->source) !== 0 && strpos($url, 'http://' . $this->source) !== 0)
		{
			return true;
		}
		
		// If dlid is already added we do not need to do anything
		if (strpos($url, 'dlid=') !== false)
		{
			return true;
		}
		
		if (!$extension = $this->getExtension())
		{
			return true;
		}
		
		if (!$dlk = $extension->params->get('dlk', null))
		{
			JFactory::getApplication()->enqueueMessage('Download key not found.', 'warning');
			return true;
		}
		
		if (strpos($url, '?') === false)
		{
			$url .= '?dlid=' . $dlk;
		}
		else
		{
			$url .= '&dlid=' . $dlk;
		}

		return true;
	}
	
	protected function getExtension()
	{
		if ($this->updates === null)
		{
			$app  = JFactory::getApplication();
			$uids = $app->input->get('cid', array(), 'array');
			$uids = array_filter(array_map('intval', $uids));
			
			if (empty($uids))
			{
				return false;
			}
			
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			
			$query->select($db->quoteName(array('u.update_id', 'u.detailsurl', 'e.extension_id', 'e.element', 'e.params')))
				->from($db->quoteName('#__updates') . ' AS '. $db->quoteName('u'))
				->join('INNER', $db->quoteName('#__extensions') . ' AS '. $db->quoteName('e') . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id'))
				->where($db->quoteName('u.update_id') . ' IN (' . implode(',', $uids) . ')');

			$db->setQuery($query);
			$updates = $db->loadObjectList('update_id');
			
			if ($ucount = count($updates))
			{
				if ($ucount > 1)
				{
					// Sort the updates as per order of uids array
					$updates = $this->sortArrayByArray($updates, $uids);
				}

				// Now reset the keys
				$this->updates = array_values($updates);
			}
			else
			{
				$this->updates = array();
			}
		}
		
		if ($this->processed > 0)
		{
			$key       = ($this->processed - 1);
			$extension = isset($this->updates[$key]) ? $this->updates[$key] : false;
		}
		else
		{
			$extension = isset($this->updates[0]) ? $this->updates[0] : false;
		}
		
		if (is_object($extension))
		{
			$params = new JRegistry;
			$params->loadString($extension->params);
			
			if ($download_key = $params->get('download_key', null))
			{
				$data = self::decodeData($download_key);
				$params->set('dlk', $data['dlk']);
			}
			
			$extension->params = $params;
		}
		
		return $extension;
	}
	
	public static function decodeData($string, $renewHost = false)
	{
		$data = array('host' => '', 'dlk' => '', 'uname' => '', 'access' => 0, 'last_checked' => '');

		if (empty($string) || !is_string($string))
		{
			return $data;
		}
		
		$string = self::decode($string);
		
		if (empty($string) || !is_string($string))
		{
			return $data;
		}
		
		$parts  = !empty($string) && strpos($string, '|*|') !== false ? explode('|*|', $string) : array();
		$newHost = self::getHost();
		
		if ($renewHost)
		{
			$data['host']         = $newHost;
			$data['dlk']          = isset($parts[1]) ? self::cleanDlk($parts[1]) : '';
			$data['uname']        = isset($parts[2]) ? $parts[2] : '';
			$data['access']       = isset($parts[3]) ? intval($parts[3]) : 0;
			$data['last_checked'] = isset($parts[4]) ? $parts[4] : null;
		}
		else
		{
			$host = '';
			
			if (!empty($parts[0]))
			{
				$host = self::decode($parts[0]);
				
				if ($host === false)
				{
					$host = $parts[0];
				}
			}
			
			$prefix = 'www.';
			
			if (!empty($host) && substr($host, 0, strlen($prefix)) == $prefix)
			{
				$host = substr($host, strlen($prefix));
			}

			if (!empty($newHost) && substr($newHost, 0, strlen($prefix)) == $prefix)
			{
				$newHost = substr($newHost, strlen($prefix));
			}

			if (!empty($newHost) && $host == $newHost)
			{
				$data['host']         = $newHost;
				$data['dlk']          = isset($parts[1]) ? self::cleanDlk($parts[1]) : '';
				$data['uname']        = isset($parts[2]) ? $parts[2] : '';
				$data['access']       = isset($parts[3]) ? intval($parts[3]) : 0;
				$data['last_checked'] = isset($parts[4]) ? $parts[4] : null;
			}
		}
		
		return $data;
	}
	
	protected static function getHost($host = null)
	{
		$host = $host ? $host : JUri::root();
		
		if (empty($host))
		{
			return '';
		}
		
		$parts = parse_url($host);
		
		$result = '';
		
		if (!empty($parts['host']))
		{
			$result .= $parts['host'];
		}
		
		if (!empty($parts['path']))
		{
			$result .= $parts['path'];
		}
		
		return $result;
	}
	
	protected function saveParams($download_key)
	{
		if (empty($this->component) && empty($this->package))
		{
			$this->setError('Component information not found.');
			return false;
		}
		
		$db    = JFactory::getDbo();
		$query = $db->getQuery(true);
		
		if (!empty($this->component))
		{
			$params = $this->params;
			$params->set('download_key', $download_key);
			
			$query->update($db->quoteName('#__extensions'))
	      ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
	      ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
	      ->where($db->quoteName('element') . ' = ' . $db->quote($this->component));
			$db->setQuery($query);
			
			try
			{
				$result = $db->execute();
			}
			catch (Exception $e)
			{
				$this->setError($e->getMessage());
				return false;
			}
		}
		
		if (!empty($this->package))
		{
			$params = new JRegistry;
			$params->set('download_key', $download_key);
			
			$query->clear()
	      ->update($db->quoteName('#__extensions'))
	      ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
	      ->where($db->quoteName('type') . ' = ' . $db->quote('package'))
	      ->where($db->quoteName('element') . ' = ' . $db->quote($this->package));
			$db->setQuery($query); 
			
			try
			{
				$result = $db->execute();
			}
			catch (Exception $e)
			{
				$this->setError($e->getMessage());
				return false;
			}
		}
		
		return true;
	}
	
	protected function refreshUpdates($dlk)
	{
		$app = JFactory::getApplication();
			
		if (empty($this->package) || empty($dlk))
		{
			return false;
		}
	
		if (!$dlk = self::cleanDlk($dlk))
		{
			return false;
		}
		
		$db          = JFactory::getDbo();
		$extra_query = 'dlid=' . $dlk;
		
		// Get the update sites for current extension
		$query = $db->getQuery(true);
    $query->select($db->quoteName('a.update_site_id'))
      ->from($db->quoteName('#__update_sites_extensions') . ' AS a')
      ->join('LEFT', $db->quoteName('#__extensions') . ' AS b ON ' . $db->quoteName('b.extension_id') . ' = ' . $db->quoteName('a.extension_id'))
      ->where($db->quoteName('b.type') . ' = ' . $db->quote('package'))
      ->where($db->quoteName('b.element') . ' = ' . $db->quote($this->package));
		try
		{
			$db->setQuery($query);
			$updateSiteIDs = $db->loadColumn(0);
		}
		catch (Exception $e)
		{
			$this->setError($e->getMessage());
			return false;
		}
		
		if (count($updateSiteIDs))
		{
			foreach ($updateSiteIDs as $id)
			{
				$query->clear()
		      ->select('*')
		      ->from('#__update_sites')
		      ->where('update_site_id = ' . (int) $id);
				$db->setQuery($query);
				
				try
				{
					$updateSite = $db->loadObject();
				}
				catch (Exception $e)
				{
					$this->setError($e->getMessage());
					return false;
				}
				
				if (!is_object($updateSite))
				{
					continue;
				}

				// Do we have the extra_query property (J 3.2+) and does it match?
				if (property_exists($updateSite, 'extra_query'))
				{
					if ($updateSite->extra_query == $extra_query)
					{
						continue;
					}
				}
				else
				{
					// Joomla! 3.1 or earlier. Updates may or may not work.
					continue;
				}
				
				$updateSite->update_site_id = $id;
				$updateSite->extra_query    = $extra_query;
				
				try
				{
					$db->updateObject('#__update_sites', $updateSite, 'update_site_id', true);
				}
				catch (Exception $e)
				{
					$this->setError($e->getMessage());
					return false;
				}
			}
		}
		
		return true;
	}
	
	/**
	* Method to return JSON object values with proper header
	* 
	* @param arry $message Array to be return as JSON object
	* 
	* @return void
	*/
	protected function doReturn($output) 
	{
		$app     = JFactory::getApplication();
		$obLevel = ob_get_level();
		
		if ($obLevel)
		{
			while ($obLevel > 0)
			{
				@ob_end_clean();
				$obLevel --;
			}
		}
		elseif (ob_get_contents())
		{
			@ob_clean();
		}
		
		header('Content-type: application/text');
		header('Content-type: application/json');
		header('Cache-Control: public,max-age=1,must-revalidate');
		header('Expires: ' . gmdate('D, d M Y H:i:s', ($_SERVER['REQUEST_TIME'] + 1)) . ' GMT');
		header('Last-modified: ' . gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME']) . ' GMT');
		
		if (function_exists('header_remove')) 
		{
			header_remove('Pragma');
		}
		
		$this->time_end = microtime(true);
		$execution_time = ($this->time_end - $this->time_start);
		
		if ($execution_time < 1)
		{
			$execution_time = number_format(($execution_time * 1000), 2, '.', ',') . ' ms';
		}
		else
		{
			$execution_time = number_format($execution_time, 6, '.', ',') . ' s';
		}
		
		$output = (array) $output;
		$output['execution_time'] = $execution_time;
		
		echo json_encode($output);
		
		flush();
		$app->close();
	}
	
	protected static function cleanDlk($dlk)
	{
		if (empty($dlk))
		{
			return '';
		}
		
		$dlk = trim($dlk);

		if (empty($dlk))
		{
			return '';
		}
		
		// Is the Download Key too short?
		if (strlen($dlk) < 32)
		{
			return '';
		}
		
		if (strlen($dlk) > 32)
		{
			$dlk = substr($dlk, 0, 32);
		}
		
		$dlk = preg_replace("/[^a-zA-Z0-9]+/", "", $dlk);
		
		return $dlk;
	}
	
	protected function sortArrayByArray($array, $orderArray)
	{
		$array      = (array) $array;
		$orderArray = (array) $orderArray;
		$ordered    = array();
		
		foreach ($orderArray as $key)
		{
			if (array_key_exists($key, $array))
			{
				$ordered[$key] = $array[$key];
				unset($array[$key]);
			}
		}
		
		return $ordered + $array;
	}
	
	public static function encode($string)
	{
		$function = strtolower(self::$encoding . self::$encoding_suffix . '_encode');
		
		if (function_exists($function))
		{
			return call_user_func($function, $string);
		}
		
		return false;
	}
	
	public static function decode($string)
	{
		$function = strtolower(self::$encoding . self::$encoding_suffix . '_decode');
		
		if (function_exists($function))
		{
			return call_user_func($function, $string);
		}
		
		return false;
	}
	
	public static function getEncodeFilter()
	{
		return strtoupper(self::$encoding . self::$encoding_suffix);
	}
}