<?php
/**
 * @package   AkeebaBackup
 * @copyright Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Backup\Admin\Model;

// Protect from unauthorized access
defined('_JEXEC') or die();

use Exception;
use FOF30\Container\Container;
use FOF30\Update\Update;
use JFile;
use JLoader;

/**
 * Updates model. Acts as an intermediary between the component and Joomla!,
 *
 * @package Akeeba\Backup\Admin\Model
 */
class Updates extends Update
{
	/**
	 * Obsolete update site locations
	 *
	 * @var  array
	 */
	protected $obsoleteUpdateSiteLocations = array(
		'http://cdn.akeebabackup.com/updates/abpro.xml',
		'http://cdn.akeebabackup.com/updates/abcore.xml',
		'http://cdn.akeebabackup.com/updates/fof.xml',
	);

	/**
	 * Public constructor. Initialises the protected members as well.
	 *
	 * @param array $config
	 */
	public function __construct($config = array())
	{
		$container = Container::getInstance('com_akeeba');

		$config['update_component']  = 'pkg_akeeba';
		$config['update_sitename']   = 'Akeeba Backup Core';
		$config['update_site']       = 'https://cdn.akeebabackup.com/updates/pkgakeebacore.xml';
		$config['update_extraquery'] = '';

		$isPro = defined('AKEEBA_PRO') ? AKEEBA_PRO : 0;
		$dlid = $container->params->get('update_dlid', '');

		// If I have a valid Download ID I will need to use a non-blank extra_query in Joomla! 3.2+
		if (preg_match('/^([0-9]{1,}:)?[0-9a-f]{32}$/i', $dlid))
		{
			// Even if the user entered a Download ID in the Core version. Let's switch his update channel to Professional
			$isPro = true;
		}

		if ($isPro)
		{
			$config['update_sitename']   = 'Akeeba Backup Professional';
			$config['update_site']       = 'https://cdn.akeebabackup.com/updates/pkgakeebapro.xml';
			$config['update_extraquery'] = 'dlid=' . $dlid;
		}

		if (defined('AKEEBA_VERSION') && !in_array(substr(AKEEBA_VERSION, 0, 3), ['dev', 'rev']))
		{
			$config['update_version'] = AKEEBA_VERSION;
		}

		parent::__construct($config);

		$this->container = $container;

		$this->extension_id = $this->findExtensionId('pkg_akeeba', 'package');

		if (empty($this->extension_id))
		{
			$this->createFakePackageExtension();
			$this->extension_id = $this->findExtensionId('pkg_akeeba', 'package');
		}
	}

	/**
	 * Refreshes the update sites, removing obsolete update sites in the process
	 */
	public function refreshUpdateSite()
	{
		// Remove any update sites for the old com_akeeba package
		$this->removeObsoleteComponentUpdateSites();

		// Refresh our update sites
		parent::refreshUpdateSite();
	}

	/**
	 * Handle automatic updates. Sends update notification emails and/or installs a new version automatically.
	 *
	 * @return  array
	 */
	public function autoupdate()
	{
		$return = array(
			'message' => ''
		);

		// First of all let's check if there are any updates
		$updateInfo = (object) $this->getUpdates(true);

		// There are no updates, there's no point in continuing
		if (!$updateInfo->hasUpdate)
		{
			return array(
				'message' => array("No available updates found")
			);
		}

		$return['message'][] = "Update detected, version: " . $updateInfo->version;

		// Ok, an update is found, what should I do?
		$params     = $this->container->params;
		$autoupdate = $params->get('autoupdateCli', 1);

		// Let's notifiy the user
		if ($autoupdate == 1 || $autoupdate == 2)
		{
			$email = $params->get('notificationEmail');

			if (!$email)
			{
				$return['message'][] = "There isn't an email for notifications, no notification will be sent.";
			}
			else
			{
				// Ok, I can send it out, but before let's check if the user set any frequency limit
				$numfreq    = $params->get('notificationFreq', 1);
				$freqtime   = $params->get('notificationTime', 'day');
				$lastSend   = $this->getLastSend();
				$shouldSend = false;

				if (!$numfreq)
				{
					$shouldSend = true;
				}
				else
				{
					$check = strtotime('-' . $numfreq . ' ' . $freqtime);

					if ($lastSend < $check)
					{
						$shouldSend = true;
					}
					else
					{
						$return['message'][] = "Frequency limit hit, I won't send any email";
					}
				}

				if ($shouldSend)
				{
					if ($this->sendNotificationEmail($updateInfo->version, $email))
					{
						$return['message'][] = "E-mail(s) correctly sent";
					}
					else
					{
						$return['message'][] = "An error occurred while sending e-mail(s). Please double check your settings";
					}

					$this->setLastSend();
				}
			}
		}

		// Let's download and install the latest version
		if ($autoupdate == 1 || $autoupdate == 3)
		{
			$return['message'][] = $this->updateComponent();
		}

		return $return;
	}

	/**
	 * Sends an update notification email
	 *
	 * @param   string $version The newest available version
	 * @param   string $email   The email address of the recipient
	 *
	 * @return  boolean  The result from JMailer::send()
	 */
	public function sendNotificationEmail($version, $email)
	{
		$email_subject = <<<ENDSUBJECT
THIS EMAIL IS SENT FROM YOUR SITE "[SITENAME]" - Update available
ENDSUBJECT;

		$email_body = <<<ENDBODY
This email IS NOT sent by the authors of Akeeba Backup. It is sent automatically
by your own site, [SITENAME]

================================================================================
UPDATE INFORMATION
================================================================================

Your site has determined that there is an updated version of Akeeba Backup
available for download.

New version number: [VERSION]

This email is sent to you by your site to remind you of this fact. The authors
of the software will never contact you about available updates.

================================================================================
WHY AM I RECEIVING THIS EMAIL?
================================================================================

This email has been automatically sent by a CLI script you, or the person who built
or manages your site, has installed and explicitly activated. This script looks
for updated versions of the software and sends an email notification to all
Super Users. You will receive several similar emails from your site, up to 6
times per day, until you either update the software or disable these emails.

To disable these emails, please contact your site administrator.

If you do not understand what this means, please do not contact the authors of
the software. They are NOT sending you this email and they cannot help you.
Instead, please contact the person who built or manages your site.

================================================================================
WHO SENT ME THIS EMAIL?
================================================================================

This email is sent to you by your own site, [SITENAME]

ENDBODY;

		$jconfig  = $this->container->platform->getConfig();
		$sitename = $jconfig->get('sitename');

		$substitutions = array(
			'[VERSION]'  => $version,
			'[SITENAME]' => $sitename
		);

		$email_subject = str_replace(array_keys($substitutions), array_values($substitutions), $email_subject);
		$email_body    = str_replace(array_keys($substitutions), array_values($substitutions), $email_body);

		try
		{
			$mailer = \JFactory::getMailer();

			$mailfrom = $jconfig->get('mailfrom');
			$fromname = $jconfig->get('fromname');

			$mailer->setSender(array($mailfrom, $fromname));
			$mailer->addRecipient($email);
			$mailer->setSubject($email_subject);
			$mailer->setBody($email_body);

			return $mailer->Send();
		}
		catch (\Exception $e)
		{
			// Joomla! 3.5 is written by incompetent bonobos
			return false;
		}
	}

	/**
	 * Automatically download and install the updated version
	 *
	 * @return  string  The message to show in the CLI output
	 */
	private function updateComponent()
	{
		\JLoader::import('joomla.updater.update');

		$db = $this->container->db;

		$updateSiteIDs = $this->getUpdateSiteIds();
		$update_site   = array_shift($updateSiteIDs);

		$query = $db->getQuery(true)
					->select($db->qn('update_id'))
					->from($db->qn('#__updates'))
					->where($db->qn('update_site_id') . ' = ' . $update_site);

		$uid = $db->setQuery($query)->loadResult();

		$update   = new \JUpdate();
		$instance = \JTable::getInstance('update');
		$instance->load($uid);
		$update->loadFromXML($instance->detailsurl);

		if (isset($update->get('downloadurl')->_data))
		{
			$url = trim($update->downloadurl->_data);
		}
		else
		{
			return "No download URL found inside XML manifest";
		}

		$extra_query = $instance->extra_query;

		if ($extra_query)
		{
			if (strpos($url, '?') === false)
			{
				$url .= '?';
			}
			else
			{
				$url .= '&amp;';
			}

			$url .= $extra_query;
		}

		$config   = $this->container->platform->getConfig();
		$tmp_dest = $config->get('tmp_path');

		if (!$tmp_dest)
		{
			return "Joomla temp directory is empty, please set it before continuing";
		}
		elseif (!\JFolder::exists($tmp_dest))
		{
			return "Joomla temp directory does not exists, please set the correct path before continuing";
		}

		$p_file = \JInstallerHelper::downloadPackage($url);

		if (!$p_file)
		{
			return "An error occurred while trying to download the latest version";
		}

		// Unpack the downloaded package file
		$package = \JInstallerHelper::unpack($tmp_dest . '/' . $p_file);

		if (!$package)
		{
			return "An error occurred while unpacking the file, please double check your Joomla temp directory";
		}

		$installer = new \JInstaller;
		$installed = $installer->install($package['extractdir']);

		// Let's cleanup the downloaded archive and the temp folder
		if (\JFolder::exists($package['extractdir']))
		{
			\JFolder::delete($package['extractdir']);
		}

		if (\JFile::exists($package['packagefile']))
		{
			\JFile::delete($package['packagefile']);
		}

		if ($installed)
		{
			return "Component successfully updated";
		}
		else
		{
			return "An error occurred while trying to update the component";
		}
	}
	
	/**
	 * Does the user need to provide FTP credentials? It also registers any FTP credentials provided in the URL.
	 *
	 * @return  bool  True if the user needs to provide FTP credentials
	 */
	public function needsFTPCredentials()
	{
		// Determine wether FTP credentials have been passed along with the current request
		\JLoader::import('joomla.client.helper');

		$user = $this->input->get('username', null, 'raw');
		$pass = $this->input->get('password', null, 'raw');

		if (!(($user == '') && ($pass == '')))
		{
			// Add credentials to the session
			if (\JClientHelper::setCredentials('ftp', $user, $pass))
			{
				return false;
			}

			return true;
		}

		return !\JClientHelper::hasCredentials('ftp');
	}

	/**
	 * Get the UNIX timestamp of the the last time we sent out an update notification email
	 *
	 * @return  integer
	 */
	private function getLastSend()
	{
		return $this->container->params->get('akeebasubs_autoupdate_lastsend', 0);
	}

	/**
	 * Set the UNIX timestamp of the last time we sent out an update notificatin email to be right now
	 *
	 * @return  void
	 */
	private function setLastSend()
	{
		$this->container->params->set('akeebasubs_autoupdate_lastsend', time());
		$this->container->params->save();
	}

	/**
	 * Removes the obsolete update sites for the component, since now we're dealing with a package.
	 *
	 * Controlled by componentName, packageName and obsoleteUpdateSiteLocations
	 *
	 * Depends on getExtensionId, getUpdateSitesFor
	 *
	 * @return  void
	 */
	private function removeObsoleteComponentUpdateSites()
	{
		// Initialize
		$deleteIDs      = array();

		// Get component ID
		$componentID = $this->findExtensionId('com_akeeba', 'component');

		// Get package ID
		$packageID = $this->findExtensionId('pkg_akeeba', 'package');

		// Update sites for old extension ID (all)
		if ($componentID)
		{
			// Old component packages
			$moreIDs   = $this->getUpdateSitesFor($componentID, null);

			if (is_array($moreIDs) && count($moreIDs))
			{
				$deleteIDs = array_merge($deleteIDs, $moreIDs);
			}

			// Obsolete update sites
			$moreIDs   = $this->getUpdateSitesFor(null, $componentID, $this->obsoleteUpdateSiteLocations);

			if (is_array($moreIDs) && count($moreIDs))
			{
				$deleteIDs = array_merge($deleteIDs, $moreIDs);
			}
		}

		// Update sites for any but current extension ID, location matching any of the obsolete update sites
		if ($packageID)
		{
			// Update sites for all of the current extension ID update sites
			$moreIDs   = $this->getUpdateSitesFor($packageID, null);

			if (is_array($moreIDs) && count($moreIDs))
			{
				$deleteIDs = array_merge($deleteIDs, $moreIDs);
			}

			$deleteIDs = array_unique($deleteIDs);

			// Remove the last update site
			if (count($deleteIDs))
			{
				$lastID = array_pop($moreIDs);
				$pos = array_search($lastID, $deleteIDs);
				unset($deleteIDs[$pos]);
			}
		}

		$db        = $this->container->db;
		$deleteIDs = array_unique($deleteIDs);

		if (empty($deleteIDs) || !count($deleteIDs))
		{
			return;
		}

		$deleteIDs = array_map(array($db, 'q'), $deleteIDs);

		$query = $db->getQuery(true)
		            ->delete($db->qn('#__update_sites'))
		            ->where($db->qn('update_site_id') . ' IN(' . implode(',', $deleteIDs) . ')');

		try
		{
			$db->setQuery($query)->execute();
		}
		catch (Exception $e)
		{
			// Do nothing.
		}

		$query = $db->getQuery(true)
		            ->delete($db->qn('#__update_sites_extensions'))
		            ->where($db->qn('update_site_id') . ' IN(' . implode(',', $deleteIDs) . ')');

		try
		{
			$db->setQuery($query)->execute();
		}
		catch (Exception $e)
		{
			// Do nothing.
		}
	}

	/**
	 * Gets the ID of an extension
	 *
	 * @param   string  $element  Extension element, e.g. com_foo, mod_foo, lib_foo, pkg_foo or foo (CAUTION: plugin, file!)
	 * @param   string  $type     Extension type: component, module, library, package, plugin or file
	 * @param   null    $folder   Plugins: plugin folder. Modules: admin/site
	 *
	 * @return  int  Extension ID or 0 on failure
	 */
	private function findExtensionId($element, $type = 'component', $folder = null)
	{
		$db = $this->container->db;
		$query = $db->getQuery(true)
		            ->select($db->qn('extension_id'))
		            ->from($db->qn('#__extensions'))
		            ->where($db->qn('element') . ' = ' . $db->q($element))
		            ->where($db->qn('type') . ' = ' . $db->q($type));

		// Plugin? We should look for a folder
		if ($type == 'plugin')
		{
			$folder = empty($folder) ? 'system' : $folder;

			$query->where($db->qn('folder') . ' = ' . $db->q($folder));
		}

		// Module? Use the folder to determine if it's site or admin module.
		if ($type == 'module')
		{
			$folder = empty($folder) ? 'site' : $folder;

			$query->where($db->qn('client_id') . ' = ' . $db->q(($folder == 'site') ? 0 : 1));
		}

		try
		{
			$id = $db->setQuery($query, 0, 1)->loadResult();
		}
		catch (Exception $e)
		{
			$id = 0;
		}

		return empty($id) ? 0 : (int) $id;
	}

	/**
	 * Returns the update site IDs matching the criteria below. All criteria are optional but at least one must be
	 * defined for the method call to make any sense.
	 *
	 * @param   int|null  $includeEID  The update site must belong to this extension ID
	 * @param   int|null  $excludeEID  The update site must NOT belong to this extension ID
	 * @param   array     $locations   The update site must match one of these locations
	 *
	 * @return  array  The IDs of the update sites
	 */
	private function getUpdateSitesFor($includeEID = null, $excludeEID = null, $locations = array())
	{
		$db = $this->container->db;
		$query = $db->getQuery(true)
		            ->select($db->qn('s.update_site_id'))
		            ->from($db->qn('#__update_sites', 's'));

		if (!empty($locations))
		{
			$quotedLocations = array_map(array($db, 'q'), $locations);
			$query->where($db->qn('location') . 'IN(' . implode(',', $quotedLocations) . ')');
		}

		if (!empty($includeEID) || !empty($excludeEID))
		{
			$query->innerJoin($db->qn('#__update_sites_extensions', 'e') . 'ON(' . $db->qn('e.update_site_id') .
			                  ' = ' . $db->qn('s.update_site_id') . ')'
			);
		}

		if (!empty($includeEID))
		{
			$query->where($db->qn('e.extension_id') . ' = ' . $db->q($includeEID));
		}
		elseif (!empty($excludeEID))
		{
			$query->where($db->qn('e.extension_id') . ' != ' . $db->q($excludeEID));
		}

		try
		{
			$ret = $db->setQuery($query)->loadColumn();
		}
		catch (Exception $e)
		{
			$ret = null;
		}

		return empty($ret) ? array() : $ret;
	}

	private function createFakePackageExtension()
	{
		$db = $this->container->db;

		$query = $db->getQuery(true)
		            ->insert($db->qn('#__extensions'))
		            ->columns(array(
			            $db->qn('name'), $db->qn('type'), $db->qn('element'), $db->qn('folder'), $db->qn('client_id'),
			            $db->qn('enabled'), $db->qn('access'), $db->qn('protected'), $db->qn('manifest_cache'),
			            $db->qn('params'), $db->qn('custom_data'), $db->qn('system_data'), $db->qn('checked_out'),
			            $db->qn('checked_out_time'), $db->qn('ordering'), $db->qn('state')
		            ))
		            ->values(array(
			            $db->q('Akeeba Backup package') . ',' .
			            $db->q('package') . ',' .
			            $db->q('pkg_akeeba') . ',' .
			            $db->q('') . ',' .
			            $db->q(0) . ',' .
			            $db->q(1) . ',' .
			            $db->q(1) . ',' .
			            $db->q(0) . ',' .
			            $db->q('{"name":"Akeeba Backup package","type":"package","creationDate":"2016-04-21","author":"Nicholas K. Dionysopoulos","copyright":"Copyright (c)2006-2017 Akeeba Ltd \/ Nicholas K. Dionysopoulos","authorEmail":"","authorUrl":"","version":"' . $this->version . '","description":"Akeeba Backup installation package, for updating from version 4.x only","group":"","filename":"pkg_akeeba"}') . ',' .
			            $db->q('{}') . ',' .
			            $db->q('') . ',' .
			            $db->q('') . ',' .
			            $db->q(0) . ',' .
			            $db->q($db->getNullDate()) . ',' .
			            $db->q(0) . ',' .
			            $db->q(0)
		            ));

		try
		{
			$db->setQuery($query)->execute();
		}
		catch (\Exception $e)
		{
			// Your database if FUBAR.
			return;
		}

		$this->createFakePackageManifest();
	}

	private function createFakePackageManifest()
	{
		$path = JPATH_ADMINISTRATOR . '/manifests/packages/pkg_akeeba.xml';

		if (file_exists($path))
		{
			return;
		}


		$content = <<< XML
<?xml version="1.0" encoding="utf-8"?>
<extension version="3.3.0" type="package" method="upgrade">
    <name>Akeeba Backup package</name>
    <author>Nicholas K. Dionysopoulos</author>
    <creationDate>2016-04-20</creationDate>
    <packagename>akeeba</packagename>
    <version>{$this->version}</version>
    <url>https://www.akeebabackup.com</url>
    <packager>Akeeba Ltd</packager>
    <packagerurl>https://www.akeebabackup.com</packagerurl>
    <copyright>Copyright (c)2006-2017 Akeeba Ltd / Nicholas K. Dionysopoulos</copyright>
    <license>GNU GPL v3 or later</license>
    <description>Akeeba Backup installation package v.revD5C5D46</description>

    <files>
        <file type="component" id="com_akeeba">com_akeeba-pro.zip</file>
        <file type="file" id="file_akeeba">file_akeeba-pro.zip</file>
        <file type="plugin" group="quickicon" id="akeebabackup">plg_quickicon_akeebabackup.zip</file>
        <file type="plugin" group="system" id="akeebaupdatecheck">plg_system_akeebaupdatecheck.zip</file>
        <file type="plugin" group="system" id="backuponupdate">plg_system_backuponupdate.zip</file>
    </files>

    <scriptfile>script.akeeba.php</scriptfile>
</extension>
XML;

		if (!@file_put_contents($content, $path))
		{
			JLoader::import('joomla.filesystem.file');
			JFile::write($path, $content);
		}
	}
}
