| Current Path : /home/echanges/www/administrator/components/com_watchfulli/ |
| Current File : /home/echanges/www/administrator/components/com_watchfulli/install.watchfulli.php |
<?php
/**
* @version install.watchfulli.php 2020-05-28 zanardi
* @package Watchful Client
* @author Watchful
* @authorUrl https://watchful.net
* @copyright Copyright (c) 2012-2025 Watchful
* @license GNU/GPL v3 or later
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\UserHelper;
use Joomla\CMS\Version;
use Joomla\Database\DatabaseInterface;
defined('_JEXEC') or defined('JPATH_PLATFORM') or die;
if (!defined('WATCHFULLI_PATH'))
{
define('WATCHFULLI_PATH', JPATH_ADMINISTRATOR . '/components/com_watchfulli');
}
class com_watchfulliInstallerScript
{
/** @var Language */
private $language;
/** @var JDatabaseDriver|\Joomla\Database\DatabaseDriver */
private $db;
/** @var Joomla\CMS\Application\CMSWebApplicationInterface|Joomla\CMS\Application\CMSApplication */
private $app;
public function __construct()
{
$this->app = Factory::getApplication();
if (Version::MAJOR_VERSION === 3)
{
$this->language = Factory::getLanguage();
$this->db = Factory::getDbo();
return;
}
$this->language = $this->app->getLanguage();
$this->db = Factory::getContainer()->get(DatabaseInterface::class);
}
/**
* @param string $type
* @param object $parent
*/
public function preflight($type, $parent): bool
{
$this->language->load('com_watchfulli');
if (version_compare($this->getPhpVersion(), '7.0.0', '<'))
{
$message = Text::sprintf('COM_WATCHFULLI_INSTALL_PHP_TOO_OLD', '7.0.0');
$this->addtoLog($message);
return false;
}
return true;
}
/**
* @param string $type
* @param object $parent
*
* @throws Exception
*/
public function postflight($type, $parent)
{
require_once WATCHFULLI_PATH . '/classes/factory.php';
require_once WATCHFULLI_PATH . '/classes/watchfulli.php';
require_once WATCHFULLI_PATH . '/classes/connection.php';
require_once WATCHFULLI_PATH . '/classes/whitelistip.php';
require_once WATCHFULLI_PATH . '/classes/joomlaversion.php';
if ($type === 'uninstall')
{
return;
}
if ($type === 'update')
{
$this->cleanUpdateRecord();
$this->cleanUpdateSites();
$this->removeObsoleteFiles();
}
$this->language->load('com_watchfulli');
$watchfulSecretKey = $this->getWatchfulSecretKey($type);
$message = Text::_('COM_WATCHFULLI_INSTALL_MESSAGE')
. Text::_('COM_WATCHFULLI_INSTALL_BEFORE_FORM')
. '<p><a href="' . $this->buildAddToWatchfulUrl($watchfulSecretKey) . '" class="btn btn-primary" target="_blank">' . Text::_('COM_WATCHFULLI_ADDSITE') . '</a></p>'
. Text::_('COM_WATCHFULLI_INSTALL_BEFORE_KEY')
. '<p><input readonly="readonly" type="text" style="width:250px;" size="55" value="' . $watchfulSecretKey . '" /></p>'
. Text::_('COM_WATCHFULLI_INSTALL_AFTER_KEY');
if (!$this->isFopenEnabled())
{
$message .= Text::_('COM_WATCHFULLI_INSTALL_NO_FOPEN');
}
if (!$this->isMaxExecutionTimeSetAsSuggested())
{
$message .= Text::_('COM_WATCHFULLI_INSTALL_NO_MAX_EXECUTION_TIME');
}
if (!$this->isCurlEnabled())
{
$message .= Text::_('COM_WATCHFULLI_INSTALL_NO_CURL');
}
if (!$this->isZipEnabled())
{
$message .= Text::_('COM_WATCHFULLI_INSTALL_NO_ZIP');
}
if (!$this->isTimeSynchronizedWithWatchful())
{
$message .= Text::_('COM_WATCHFULLI_INSTALL_NO_TIME_SYNC');
}
$this->app->enqueueMessage($message);
// Add Watchful IPs in Admintools & RSFirewall config
$this->whiteListWatchful();
}
private function buildAddToWatchfulUrl($watchfulSecretKey): string
{
require_once WATCHFULLI_PATH . '/classes/helper.php';
$query = http_build_query(
array(
'name' => $this->app->get('sitename'),
'access_url' => Uri::root(),
'secret_word' => $watchfulSecretKey,
'word_akeeba' => WatchfulliHelper::getAkeebaSecretKey(),
'cms' => 'joomla',
)
);
return 'https://app.watchful.net/app/?' . $query . '#/dashboard/';
}
/**
* Delete previously existing update records
*/
private function cleanUpdateRecord()
{
$this->db->setQuery("DELETE FROM #__updates WHERE element = 'com_watchfulli'");
$this->db->execute();
}
/**
* Delete previously existing update sites if there are more than ones
*
* @return void
*/
private function cleanUpdateSites()
{
$this->db->setQuery("SELECT COUNT(*) FROM #__update_sites WHERE name = 'Watchfully Slave Update'");
if ($this->db->loadResult() <= 1)
{
return;
}
$this->db->setQuery("DELETE FROM #__update_sites WHERE name = 'Watchfully Slave Update'");
$this->db->execute();
}
private function removeObsoleteFiles()
{
$sitePath = JPATH_SITE . '/components/com_watchfulli';
$adminPath = JPATH_ADMINISTRATOR . '/components/com_watchfulli';
$obsoleteFiles = [
$adminPath . '/classes/controller.php' => 'file',
$adminPath . '/classes/encrypt.php' => 'file',
$adminPath . '/classes/file.php' => 'file',
$adminPath . '/classes/view.php' => 'file',
$adminPath . '/views/watchfulli/tmpl/default.php' => 'file',
$adminPath . '/views/watchfulli/tmpl' => 'folder',
$adminPath . '/views/watchfulli/view.html.php' => 'file',
$adminPath . '/views/watchfulli' => 'folder',
$sitePath . '/views/watchfulli/view.json.php' => 'file',
$sitePath . '/views/watchfulli' => 'folder',
];
foreach ($obsoleteFiles as $path => $type)
{
if ($type === 'file')
{
if (file_exists($path))
{
@unlink($path);
}
}
else
{
if (is_dir($path))
{
rmdir($path);
}
}
}
}
/**
* Fetches the secret key or creates one if empty
*/
private function getWatchfulSecretKey(string $type = 'install'): string
{
jimport('joomla.user.helper');
$app = Factory::getApplication();
$params = ComponentHelper::getParams('com_watchfulli');
$current_secret_key = $params->get('secret_key');
$new_secret_key = md5(UserHelper::genRandomPassword(32));
$old_generated_key = md5('watch' . $app->get('secret') . 'fulli');
// for a new install
if ($type === 'install')
{
$this->saveJsonSecret($new_secret_key);
return $new_secret_key;
}
if (empty($current_secret_key) || $current_secret_key == $old_generated_key)
{
// this is an update - we must update the key on the master too
if (!$this->updateMaster($old_generated_key, $new_secret_key))
{
// If the connection to the watchful API fails, we alert the customer that the client is broken
$app->enqueueMessage(Text::_('COM_WATCHFULLI_INSTALL_SECRETKEY_UPDATE_MASTER_FAILED'), 'alert');
}
$this->saveJsonSecret($new_secret_key);
return $new_secret_key;
}
return $current_secret_key;
}
private function callWatchfulApi(string $endpoint, array $data = []): stdClass
{
$uri = 'https://app.watchful.net/api/v1/' . $endpoint;
$ch = curl_init();
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_TIMEOUT, 45);
curl_setopt($ch, CURLOPT_REFERER, Uri::root());
curl_setopt($ch, CURLOPT_USERAGENT, 'Watchfulli/1.0 (+http://www.watchful.net)');
curl_setopt($ch, CURLOPT_URL, $uri);
switch ('GET')
{
case 'POST':
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_POST, true);
break;
case 'DELETE':
case 'PUT':
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
break;
default:
if (!empty($data))
{
curl_setopt($ch, CURLOPT_URL, $uri . '?' . http_build_query($data));
}
}
$response = new stdClass();
$response->data = curl_exec($ch);
$response->info = curl_getinfo($ch);
$response->error = curl_error($ch);
curl_close($ch);
return $response;
}
/**
* @param $response
*
* @return bool|mixed
*/
private function decodeWatchfulResponse($response)
{
if (!is_object($response))
{
$this->addtoLog('[decodeWatchfulResponse] Response is not an object');
return false;
}
$data = json_decode($response->data);
if (json_last_error() !== JSON_ERROR_NONE)
{
$this->addtoLog('[updateMaster] Response is not a JSON string');
return false;
}
if (empty($data->msg))
{
$this->addtoLog('[updateMaster] Response msg is empty');
return false;
}
return $data->msg;
}
private function isTimeSynchronizedWithWatchful(): bool
{
if (!$this->isCurlEnabled())
{
return false;
}
$systemTime = time();
$response = $this->callWatchfulApi('time');
$data = $this->decodeWatchfulResponse($response);
if (empty($data->time))
{
return false;
}
return ($data->time > $systemTime - WatchfulliHelper::MAX_TIME_DEVIATION) && ($data->time < $systemTime + WatchfulliHelper::MAX_TIME_DEVIATION);
}
private function isMaxExecutionTimeSetAsSuggested(): bool
{
$maxExecutionTimeValue = ini_get('max_execution_time');
return $maxExecutionTimeValue >= 120 || $maxExecutionTimeValue === '0';
}
private function isFopenEnabled(): bool
{
return in_array(ini_get('allow_url_fopen'), ['On', '1'], true);
}
private function isCurlEnabled(): bool
{
return extension_loaded('curl');
}
private function isZipEnabled(): bool
{
return extension_loaded('zip');
}
/**
* This method calls the Watchful server the save a new key without the user
* needing to manually edit the site on Watchful dashboard. This only
* happens when user updates an existing Watchful client and the site is
* still using the old, relatively less secure key.
*
* We don't like to make "home calls" but the JED insisted that it was not
* enough to generate new keys on new installs and give existing clients the
* ability to refresh.
*
* With this "home call" we generate and save a new secret key for you and
* you won't have to do anything.
*/
private function updateMaster(string $old_generated_key, string $new_secret_key): bool
{
$response = $this->callWatchfulApi(
'changekey',
[
'key' => $old_generated_key,
'url' => urlencode(Uri::root()),
'newkey' => $new_secret_key,
]
);
return $this->decodeWatchfulResponse($response) !== false;
}
private function addtoLog(string $message)
{
Log::add($message, Log::DEBUG, 'watchful');
}
/**
* Save the secret as component parameter in JSON format
*
* @param string $secret
*
* @todo probably we should use Joomla framework commands instead of manual
* saves
*
*/
private function saveJsonSecret(string $secret)
{
try
{
$params = $this->db->setQuery(
$this->db->getQuery(true)
->select('params')
->from('#__extensions')
->where($this->db->quoteName('element') . ' = ' . $this->db->quote('com_watchfulli'))
)->loadResult();
if (empty($params))
{
$params = '{}';
}
$json = json_decode($params);
if (isset($json->secret_key) && $secret === $json->secret_key && !empty($secret))
{
return;
}
$json->secret_key = $secret;
$this->db->setQuery(
$this->db->getQuery(true)
->update('#__extensions')
->set($this->db->quoteName('params') . ' = ' . $this->db->quote(json_encode($json)))
->where($this->db->quoteName('element') . ' = ' . $this->db->quote('com_watchfulli'))
)->execute();
}
catch (Exception $e)
{
$this->app->enqueueMessage($e->getMessage(), 'error');
return;
}
}
private function getPhpVersion(): string
{
if (defined('PHP_VERSION'))
{
return PHP_VERSION;
}
if (function_exists('phpversion'))
{
return phpversion();
}
return '5.0.0';
}
/**
* Auto-Whitelist Watchful IPs after install/update
* @see admin/controller.php whitelist
*/
private function whiteListWatchful()
{
if (!$this->isCurlEnabled())
{
return;
}
$response = WatchfulliConnection::getCurl(
[
'url' => 'https://app.watchful.net/ip-v4.txt',
'timeout' => 300,
"follow_location" => false,
]
);
if (empty($response->data))
{
return;
}
$watchfuIps = explode("\n", $response->data);
// Remove mask (Watchful will ever use /32)
$watchfuIps = preg_replace("/\/32/", '', $watchfuIps);
try
{
new WatchfulliWhitelistIp(json_encode($watchfuIps), 'add', false);
}
catch (Exception $e)
{
return;
}
}
}