<?php
/**
 * @version    $Id$
 * @package    JSN_Sample
 * @author     JoomlaShine Team <support@joomlashine.com>
 * @copyright  Copyright (C) 2015 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');

// Import necessary Joomla libraries
jimport('joomla.application.component.model');
jimport('joomla.filesystem.archive');
jimport('joomla.filesystem.file');
jimport('joomla.installer.installer');

// Define product version caching expiration time
defined('CHECK_UPDATE_PERIOD') OR define('CHECK_UPDATE_PERIOD',	86400);

/**
 * Model class of JSN Installer library.
 *
 * @package  JSN_Sample
 * @since    1.1.0
 */
class JSNInstallerModel extends JSNBaseModel
{
	/**
	 * Base download link.
	 *
	 * @var  string
	 */
	protected $downloadLink = 'http://www.joomlashine.com/index.php?option=com_lightcart&controller=remoteconnectauthentication&task=authenticate&tmpl=component&upgrade=yes&';

	/**
	 * Check version link.
	 *
	 * @var  string
	 */
	protected $checkLink = 'http://www.joomlashine.com/versioning/product_version.php?category=cat_extension';

	/**
	 * Parsed check update URL.
	 *
	 * @var	array
	 */
	protected static $versions;

	/**
	 * Constructor
	 *
	 * @param   array  $config  An array of configuration options.
	 */
	public function __construct($config = array())
	{
		parent::__construct($config);

		// Load language manually
		$lang = JFactory::getLanguage();
		$lang->load('jsn_installer', JPATH_COMPONENT_ADMINISTRATOR . '/libraries/joomlashine/installer');

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

		// Get input object
		$this->input = $this->app->input;

		// Get Joomla config
		$this->config = JFactory::getConfig();
	}

	/**
	 * Download dependency package.
	 *
	 * @return  string  Package name.
	 */
	public function download()
	{
		// Get dependency declaration
		$app 		= JFactory::getApplication();
		$input 		= $app->input;
		$extension 	= (object) $input->getArray($_GET);

		// Get product edition
		$edition = $this->getEdition();

		// Get Joomla version
		$JVersion = new JVersion;

		// Store username/password
		$this->app->setUserState('jsn.installer.customer.username', $this->input->getUsername('customer_username'));
		$this->app->setUserState('jsn.installer.customer.password', $this->input->getString('customer_password'));

		// Get download link
		$url = $this->getLink($extension, $JVersion);

		// Generate file name for depdendency package
		$name[]	= 'jsn';
		$name[]	= $extension->identified_name;

		if ($edition)
		{
			$name[] = strtolower(str_replace(' ', '-', isset($extension->edition) ? $extension->edition : $edition));
		}

		$name[]	= 'j' . $JVersion->RELEASE;
		$name[]	= 'install.zip';
		$name	= implode('_', $name);

		// Try to download the update package
		try
		{
			// Check if temporary directory exists
			if ( ! $this->config->get('ftp_enable') AND ( ! is_dir($this->config->get('tmp_path')) OR ! is_writable($this->config->get('tmp_path'))))
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_TEMPORARY_DIRECTORY_NOT_WRITABLE') . '|' . $url);
			}

			// Set maximum execution time
			ini_set('max_execution_time', 300);

			// Get package data
			$result = $this->fetchHttp($url);

			// Validate downloaded data
			
			if (strlen($result) < 10)
			{
				// Get LightCart error code
				throw new Exception(JText::_('JSN_EXTFW_LIGHTCART_ERROR_' . $result));
			}

			// Store downloaded package data to temporary directory
			if ( ! JFile::write($this->config->get('tmp_path') . '/' . $name, $result))
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED') . '|' . $url);
			}
		}
		catch (Exception $e)
		{
			throw $e;
		}

		return $name;
	}

	/**
	 * Install dependency package.
	 *
	 * @return  string
	 */
	public function install()
	{
		// Get dependency declaration
		$app 		= JFactory::getApplication();
		$input 		= $app->input;
		$extension 	= (object) $input->getArray($_GET);

		// Get product edition
		$edition = $this->getEdition();

		// Get Joomla version
		$JVersion = new JVersion;

		// Get download link
		$url = $this->getLink($extension, $JVersion);
		$file = $input->files->get('package', null, 'raw');

		// Finalize upload
		if (is_array($file) && count($file))
		{
			jimport('joomla.filesystem.file');
			
			if ($file['error'] || $file['size'] < 1 || $file['name'] !== JFile::makeSafe($file['name'])	)
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED'));
			}
			
			$filetypes = explode('.', $file['name']);

			if (count($filetypes) < 2)
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED'));
			}
			
			array_shift($filetypes);
			
			// Media file names should never have executable extensions buried in them.
			$executable = array(
				'php', 'js', 'exe', 'phtml', 'java', 'perl', 'py', 'asp','dll', 'go', 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp',
				'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh'
			);
			
			$check = array_intersect($filetypes, $executable);
			
			if (!empty($check))
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED'));
			}
			
			$filetype = array_pop($filetypes);
			if ($filetype == '' || $filetype == false || !in_array($filetype, array('zip', 'bz2', 'gz')))
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED'));
			}
			
			$xssCheck = file_get_contents($file['tmp_name'], false, null, -1, 256);
		
			$htmlTags = array(
				'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink',
				'blockquote', 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del',
				'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
				'head', 'hr', 'html', 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext',
				'link', 'listing', 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object',
				'ol', 'optgroup', 'option', 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar',
				'small', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
				'tr', 'tt', 'ul', 'var', 'wbr', 'xml', 'xmp', '!DOCTYPE', '!--'
			);
			
			foreach ($htmlTags as $tag)
			{
				// A tag is '<tagname ', so we need to add < and a space or '<tagname>'
				if (stristr($xssCheck, '<' . $tag . ' ') || stristr($xssCheck, '<' . $tag . '>'))
				{
					throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED'));
				}
			}

			if (!JFile::upload($file['tmp_name'], $this->config->get('tmp_path') . '/' . $file['name'], false, true))
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_PACKAGE_SAVING_FAILED'));
			}

			$this->input->set('package', $file['name']);
		}

		if ($this->input->getString('package'))
		{
			// Initialize dependency package path
			$file = $this->config->get('tmp_path') . '/' . $this->input->getString('package');
			$path = substr($file, 0, -4);

			if ( ! is_file($file))
			{
				// Check temporary directory existen
				if ( ! $this->config->get('ftp_enable') AND ( ! is_dir($this->config->get('tmp_path')) OR ! is_writable($this->config->get('tmp_path'))))
				{
					throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_TEMPORARY_DIRECTORY_NOT_WRITABLE') . '|' . $url);
				}
				else
				{
					throw new Exception('FAIL:' . JText::sprintf('JSN_EXTFW_INSTALLER_PACKAGE_NOT_FOUND', $this->input->getString('package')) . '|' . $url);
				}
			}

			$extension->source = $path;

			// Extract dependency package
			if ( ! JArchive::extract($file, $path))
			{
				throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_EXTRACT_PACKAGE_FAIL') . '|' . $url);
			}

			// Switch off debug mode to catch JInstaller error message manually
			$config	= JFactory::getConfig();
			$debug	= $config->get('debug');

			$config->set('debug', version_compare($JVersion->RELEASE, '3.0', '<') ? false : true);

			// Get JSN Installer
			require_once JPATH_COMPONENT_ADMINISTRATOR . '/subinstall.php';

			$installer = $this->input->getCmd('option') . 'InstallerScript';
			$installer = new $installer;

			try
			{
				$installer->installExtension($extension);
			}
			catch (Exception $e)
			{
				throw $e;
			}

			// Clean-up temporary folder and file
			JFolder::delete($extension->source);
			JFile::delete("{$extension->source}.zip");

			// Restore debug settings
			$config->set('debug', $debug);

			// Check if installation success
			$messages = JFactory::getApplication()->getMessageQueue();

			if (class_exists('JError'))
			{
				$messages = array_merge(JError::getErrors(), $messages);
			}

			foreach ($messages AS $message)
			{
				if (
					(is_array($message) AND @$message['type'] == 'error')
					OR
					(is_object($message) AND ( ! method_exists($message, 'get') OR $message->get('level') == E_ERROR))
				)
				{
					$errors[is_array($message) ? $message['message'] : $message->getMessage()] = 1;
				}
			}

			if (@count($errors))
			{
				throw new Exception('<ul><li>' . implode('</li><li>', array_keys($errors)) . '</li></ul>');
			}
		}
		else
		{
			throw new Exception('FAIL:' . JText::_('JSN_EXTFW_INSTALLER_MISSING_PACKAGE_NAME') . '|' . $url);
		}

		return 'SUCCESS';
	}

	/**
	 * Finalize dependency installation.
	 *
	 * @return  void
	 */
	public function finalize()
	{
		// Save live update notification setting to config table
		$model	= new JSNConfigModel;
		$form	= $model->getForm(array(), true);
		$data	= array('live_update_notification' => (string) $this->input->getInt('live_update_notification', 0));

		try
		{
			$model->save($form, $data);
		}
		catch (Exception $e)
		{
			throw $e;
		}
	}

	/**
	 * Check dependency.
	 *
	 * @param   array  &$dependencies  An array of dependency package.
	 * @param   bool   $checkUpdate    Whether to check for dependency update or not?
	 *
	 * @return  mixed
	 */
	public function check(&$dependencies, $checkUpdate = true)
	{
		// Initialize variables
		$missingDependency		= 0;
		$authenticationRequired	= false;

		// Get product edition
		$edition = $this->getEdition();

		// Get object for working with extension table
		$extension = JTable::getInstance('Extension');

		// Get installed Joomla version
		$JVersion = new JVersion;

		// Check dependency
		foreach ($dependencies AS & $dependency)
		{
			if ($dependency instanceof SimpleXMLElement)
			{
				$tmp = (array) $dependency;
				$tmp = (object) $tmp['@attributes'];
				$tmp->title = trim((string) $dependency != '' ? (string) $dependency : ($dependency['title'] ? (string) $dependency['title'] : $tmp->name));

				$dependency = $tmp;
			}

			// Skip dependency that is being removed
			if (isset($dependency->remove) AND $dependency->remove)
			{
				continue;
			}

			// Build dependency path
			switch ($dependency->type = strtolower($dependency->type))
			{
				case 'component':
				case 'module':
					$path = (( ! isset($dependency->client) OR $dependency->client != 'site') ? JPATH_BASE : JPATH_ROOT) . "/{$dependency->type}s";
				break;

				case 'plugin':
					$path = JPATH_ROOT . '/plugins/' . $dependency->folder;
				break;

				case 'template':
					$path = JPATH_ROOT . '/templates';
				break;
			}

			$path .= '/' . $dependency->name;

			// Check if dependency is installed
			$installed = file_exists($path) ? true : false;

			if ( ! $checkUpdate)
			{
				$installed ? ($dependency->upToDate = true) : $missingDependency++;

				// Continue immediately because checking for dependency update is disabled
				continue;
			}

			// Check if dependency has newer version
			if ($installed)
			{
				// Load dependency details
				$extension->load(
					array(
						'type'		=> $dependency->type,
						'element'	=> $dependency->name,
						'folder'	=> isset($dependency->folder) ? $dependency->folder : ''
					)
				);

				// Get currently installed dependency version
				$current = json_decode($extension->manifest_cache);
				$current = (is_object($current) AND isset($current->version)) ? $current->version : '0.0.0';
			}
			else
			{
				$current = '0.0.0';
			}

			// Get latest version for dependency
			if ( ! isset($dependency->identified_name)
				OR ! ($result = $this->hasUpdate($dependency->identified_name, $current, $JVersion->RELEASE))
				OR ($hasError = $result instanceof Exception))
			{
				// Store errors
				! isset($result) OR ! $result OR ! $hasError OR $errors[] = $result->getMessage();

				// Skip listing if dependency is up-to-date
				(version_compare($current, '0.0.0', 'gt') OR  ! isset($dependency->identified_name)) ? ($dependency->upToDate = true) : $missingDependency++;

				if (isset($dependency->upToDate) AND $dependency->upToDate)
				{
					// Update dependency tracking
					$ext = strtolower(substr($this->input->getCmd('option'), 4));
					$dep = ! empty($extension->custom_data) ? (array) json_decode($extension->custom_data) : array();

					if ( ! count($dep) OR ! in_array($ext, $dep))
					{
						$dep[] = $ext;

						try
						{
							$db	= JFactory::getDbo();
							$q	= $db->getQuery(true);

							$q->update('#__extensions');
							$q->set("custom_data = '" . json_encode($dep) . "'");
							$q->where("element = '{$dependency->name}'");
							$q->where("type = '{$dependency->type}'", 'AND');
							$extension->type != 'plugin' OR $q->where("folder = '{$dependency->folder}'", 'AND');

							$db->setQuery($q);
							$db->execute();
						}
						catch (Exception $e)
						{
							$this->app->enqueueMessage($e->getMessage(), 'warning');
						}
					}
				}
			}
			else
			{
				$missingDependency++;

				// Is authentication required?
				$authentication = false;

				if (isset($result->authentication) AND $result->authentication)
				{
					$authentication = true;
				}
				elseif (isset($result->editions))
				{
					foreach ($result->editions AS $item)
					{
						if (strcasecmp($item->edition, $edition) == 0 AND $item->authentication)
						{
							$authentication = true;
						}
					}
				}

				// Prepare for authentication
				if ($authentication)
				{
					$authenticationRequired	= true;
					$dependency->edition	= str_replace(' ', '+', trim(isset($result->edition) ? $result->edition : $edition));
				}
			}
		}

		if ($missingDependency == 0)
		{
			$this->saveDependency($dependencies);

			return -1;
		}

		return isset($errors) ? $errors : $authenticationRequired;
	}

	/**
	 * Get product edition.
	 *
	 * @return  string
	 */
	public function getEdition()
	{
		$edition = 'JSN_' . strtoupper(substr($this->input->getCmd('option'), 4)) . '_EDITION';

		if (defined($edition))
		{
			eval('$edition = ' . $edition . ';');
		}
		else
		{
			$edition = null;
		}

		return $edition;
	}

	/**
	 * Method to get latest dependency version.
	 *
	 * @param   string  $identified_name        Dependency's identified name.
	 * @param   string  $current_version        Current dependency version.
	 * @param   string  $requiredJoomlaVersion  Joomla version required by extension, e.g. 2.5, 3.0, etc.
	 * @param   object  $version                Latest version object used for recursive calls.
	 *
	 * @return  mixed  Object containing update information if dependency is outdated, FALSE otherwise.
	 */
	protected function hasUpdate($identified_name, $current_version, $requiredJoomlaVersion = JSN_SAMPLE_REQUIRED_JOOMLA_VER, $version = '')
	{
		static $result;

		// Only communicate with server if check update URLs is not load before
		if (empty($version))
		{
			if ( ! isset(self::$versions))
			{
				try
				{
					// Get Joomla config
					$config	= JFactory::getConfig();

					// Generate cache file path
					$cache = $config->get('tmp_path') . '/JoomlaShineUpdates.json';

					// Get latest version from local file if not timed out
					if (is_readable($cache) AND time() - filemtime($cache) < CHECK_UPDATE_PERIOD)
					{
						// Decode JSON encoded update details
						self::$versions = json_decode(JFile::read($cache));
					}
					else
					{
						// Always update cache file modification time
						is_readable($cache) AND touch($cache, time());

						try
						{
							self::$versions = $this->fetchHttp($this->checkLink);

							// Cache latest version to local file system
							JFile::write($cache, self::$versions);

							// Decode JSON encoded update details
							self::$versions = json_decode(self::$versions);
						}
						catch (Exception $e)
						{
							throw $e;
						}
					}
				}
				catch (Exception $e)
				{
					return $e;
				}
			}

			$version	= self::$versions;
			$result		= false;
		}

		// Get installed Joomla version
		$JVersion = new JVersion;

		// Get latest dependency version
		if ( ! $result)
		{
			foreach ($version->items AS $item)
			{
				if (isset($item->items))
				{
					$this->hasUpdate($identified_name, $current_version, $requiredJoomlaVersion, $item);
					continue;
				}

				if (isset($item->identified_name) AND $item->identified_name == $identified_name)
				{
					$result = $item;
					break;
				}
			}

			if (is_object($result))
			{
				// Does product support installed Joomla version?
				$tags = explode(';', $result->tags);

				if ( ! in_array($JVersion->RELEASE, $tags))
				{
					$result = false;
				}

				// Does product upgradable?
				if ($result AND ! empty($requiredJoomlaVersion) AND ! $this->isJoomlaCompatible($requiredJoomlaVersion) AND ! version_compare($result->version, $current_version, '>='))
				{
					$result = false;
				}

				// Does product have newer version?
				if ($result AND (empty($requiredJoomlaVersion) OR $this->isJoomlaCompatible($requiredJoomlaVersion)) AND ! version_compare($result->version, $current_version, '>'))
				{
					$result = false;
				}
			}
		}

		return $result;
	}

	/**
	 * Generate link to download dependency package.
	 *
	 * @param   object  $extension  Extension details.
	 * @param   object  $JVersion   Joomla version object.
	 *
	 * @return  string  Link to download dependency package.
	 */
	protected function getLink($extension, $JVersion)
	{
		// Build query string
		$query[] = 'joomla_version=' . $JVersion->RELEASE;
		$query[] = 'username=' . $this->app->getUserState('jsn.installer.customer.username');
		$query[] = 'password=' . $this->app->getUserState('jsn.installer.customer.password');
		$query[] = 'identified_name=' . $extension->identified_name;

		// Build final download link
		$url = $this->downloadLink . implode('&', $query);

		return $url;
	}

	/**
	 * Save dependency declaration to a constant.
	 *
	 * @param   array  &$dependencies  An array of dependency package.
	 *
	 * @return  void
	 */
	public function saveDependency(&$dependencies)
	{
		// Get component name
		$component = substr($this->input->getCmd('option'), 4);

		if ( ! defined('JSN_' . strtoupper($component) . '_DEPENDENCY'))
		{
			// Get Joomla config
			$config = JFactory::getConfig();

			// Unset some unnecessary properties
			foreach ($dependencies AS & $dependency)
			{
				unset($dependency->source);
				unset($dependency->upToDate);
			}

			$dependencies = json_encode($dependencies);

			// Store dependency declaration
			file_exists($defines = JPATH_COMPONENT_ADMINISTRATOR . '/defines.php')
				OR file_exists($defines = JPATH_COMPONENT_ADMINISTRATOR . '/' . $component . '.defines.php')
				OR $defines = JPATH_COMPONENT_ADMINISTRATOR . '/' . $component . '.php';

			if ($config->get('ftp_enable') OR is_writable($defines))
			{
				$buffer = preg_replace(
						'/(defined\s*\(\s*._JEXEC.\s*\)[^\n]+\n)/',
						'\1' . "\ndefine('JSN_" . strtoupper($component) . "_DEPENDENCY', '" . $dependencies . "');\n",
						JFile::read($defines)
				);

				JFile::write($defines, $buffer);
			}
		}
	}

	/**
	 * Get remote content via http client.
	 *
	 * @param   string  $url  URL to fetch content.
	 *
	 * @return  string  Fetched content.
	 */
	protected function fetchHttp($url)
	{
		$result = '';

		// Initialize HTTP client
		class_exists('http_class') OR require_once JPATH_COMPONENT_ADMINISTRATOR . '/libraries/3rd-party/httpclient/http.php';

		$http = new http_class;
		$http->follow_redirect		= 1;
		$http->redirection_limit	= 5;
		$http->GetRequestArguments($url, $arguments);

		// Open connection
		if (($error = $http->Open($arguments)) == '')
		{
			if (($error = $http->SendRequest($arguments)) == '')
			{
				// Get response body
				while (true)
				{
					if (($error = $http->ReadReplyBody($body, 1000)) != '' OR strlen($body) == 0)
					{
						break;
					}
					$result .= $body;
				}
			}
			else
			{
				throw new Exception($error);
			}

			// Close connection
			$http->Close();
		}
		else
		{
			throw new Exception($error);
		}

		return $result;
	}

	/**
	 * Method for checking if extension is compatible with installed Joomla version.
	 *
	 * @param   string  $requiredJoomlaVersion  Joomla version required by extension, e.g. 2.5, 3.0, etc.
	 *
	 * @return  boolean
	 */
	public static function isJoomlaCompatible($requiredJoomlaVersion)
	{
		// Get installed Joomla version
		$JVersion = new JVersion;

		// Check if installed Joomla version is compatible
		return (strpos($JVersion->getShortVersion(), $requiredJoomlaVersion) !== false);
	}
}
