Current Directory: /home/astoriaah/www/administrator/components/com_admintools/Model
Viewing File: /home/astoriaah/www/administrator/components/com_admintools/Model/Scans.php
<?php
/**
* @package admintools
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\AdminTools\Admin\Model;
defined('_JEXEC') || die;
use Akeeba\AdminTools\Admin\Model\Scanner\Crawler;
use Akeeba\AdminTools\Admin\Model\Scanner\Email;
use Akeeba\AdminTools\Admin\Model\Scanner\Logger\Logger;
use Akeeba\AdminTools\Admin\Model\Scanner\Part;
use Akeeba\AdminTools\Admin\Model\Scanner\Util\Configuration;
use Akeeba\AdminTools\Admin\Model\Scanner\Util\Session;
use Exception;
use FOF40\Container\Container;
use FOF40\Date\Date;
use FOF40\Model\DataModel;
use FOF40\Timer\Timer;
/**
* @property int $id
* @property string $comment
* @property string $scanstart
* @property string $scanend
* @property string $status
* @property string $origin
* @property int $totalfiles
*
* @property-read int $files_modified
* @property-read int $files_new
* @property-read int $files_suspicious
*
* @method $this comment() comment(string $v)
* @method $this scanstart() scanstart(string $v)
* @method $this scanend() scanend(string $v)
* @method $this status() status(string $v)
* @method $this origin() origin(string $v)
*/
class Scans extends DataModel
{
public function __construct(Container $container, array $config)
{
$config['tableName'] = '#__admintools_scans';
$config['idFieldName'] = 'id';
$config['autoChecks'] = false;
parent::__construct($container, $config);
$this->addBehaviour('Filters');
$this->addKnownField('newfile');
$this->addKnownField('files_new');
$this->addKnownField('files_modified');
$this->addKnownField('files_suspicious');
}
public function removeIncompleteScans()
{
/** @var Scans $model1 */
$model1 = $this->container->factory->model('Scans')->tmpInstance();
/** @var Scans $model2 */
$model2 = $this->container->factory->model('Scans')->tmpInstance();
/** @var DataModel\Collection $list1 */
$list1 = $model1
->status('fail')
->get();
$list2 = $model2
->status('run')
->get();
$list = $list1->merge($list2);
$list->delete();
}
/**
* Clears the table with files information
*
* @return bool
*/
public function purgeFilesCache()
{
$db = $this->getDbo();
// The best choice should be the TRUNCATE statement, however there isn't the proper function inside Joomla driver...
$query = $db->getQuery(true)
->delete($db->qn('#__admintools_filescache'));
try
{
$result = $db->setQuery($query)->execute();
}
catch (Exception $e)
{
$result = false;
}
return $result;
}
/**
* Starts a new file scan
*
* @return array
*/
public function startScan($origin = 'backend')
{
if (function_exists('set_time_limit'))
{
@set_time_limit(0);
}
// Get the scanner engine's base objects (configuration, session storage and logger)
$configuration = Configuration::getInstance();
$session = Session::getInstance();
$logger = new Logger($configuration);
// Log the start of a new scan
$logger->reset();
$logger->info(sprintf("Admin Tools Professional %s (%s)", ADMINTOOLS_VERSION, ADMINTOOLS_DATE));
$logger->info('PHP File Change Scanner');
$logger->info('Starting a new scan from the “' . $origin . '” origin.');
// Get a timer according to the engine's configuration
$maxExec = $configuration->get('maxExec');
$runtimeBias = $configuration->get('runtimeBias');
$logger->debug(sprintf("Getting a new operations timer, max. exec. time %0.2fs, runtime bias %u%%", $maxExec, $runtimeBias));
$timer = new Timer($maxExec, $runtimeBias);
// Reset the session. This marks a brand new scan.
$logger->debug('Resetting the session storage');
$session->reset();
// Create a new scan record and save its ID in the session
$logger->debug('Creating a new scan record');
$currentTime = new Date();
/** @var static $newScanRecord */
$newScanRecord = $this->tmpInstance()->create([
'scanstart' => $currentTime->toSql(),
'status' => 'run',
'origin' => $origin,
'totalfiles' => 0,
]);
$logger->debug(sprintf('Scan ID: %u', $newScanRecord->getId()));
$session->set('scanID', $newScanRecord->getId());
// Run the scanner engine
$statusArray = $this->tickScannerEngine($configuration, $session, $logger, $timer, true);
return $this->postProcessStatusArray($statusArray, $logger);
}
/**
* Steps the file scan
*
* @return array
*/
public function stepScan()
{
// Get the scanner engine's base objects (configuration, session storage and logger)
$configuration = Configuration::getInstance();
$session = Session::getInstance();
$logger = new Logger($configuration);
// Get a timer according to the engine's configuration
$maxExec = $configuration->get('maxExec');
$runtimeBias = $configuration->get('runtimeBias');
$logger->debug(sprintf("Getting a new operations timer, max. exec. time %0.2fs, runtime bias %u%%", $maxExec, $runtimeBias));
$timer = new Timer($maxExec, $runtimeBias);
// Run the scanner engine
$statusArray = $this->tickScannerEngine($configuration, $session, $logger, $timer, true);
return $this->postProcessStatusArray($statusArray, $logger);
}
protected function onAfterGetItemsArray(&$resultArray)
{
// Don't process an empty list
if (empty($resultArray))
{
return;
}
// Get the scan_id's and initialise the special fields
$scanids = [];
$map = [];
foreach ($resultArray as $index => &$row)
{
$scanids[] = $row->id;
$map[$row->id] = $index;
$row->files_new = 0;
$row->files_modified = 0;
}
// Fetch the stats for the IDs at hand
$ids = implode(',', $scanids);
$db = $this->getDbo();
$query = $db->getQuery(true)
->select([
$db->qn('scan_id'),
'(' . $db->qn('diff') . ' = ' . $db->q('') . ') AS ' . $db->qn('newfile'),
'COUNT(*) AS ' . $db->qn('count'),
])
->from($db->qn('#__admintools_scanalerts'))
->where($db->qn('scan_id') . ' IN (' . $ids . ')')
->group([
$db->qn('scan_id'),
$db->qn('newfile'),
]);
$alertstats = $db->setQuery($query)->loadObjectList();
$query = $db->getQuery(true)
->select([
$db->qn('scan_id'),
'COUNT(*) AS ' . $db->qn('count'),
])
->from($db->qn('#__admintools_scanalerts'))
->where($db->qn('scan_id') . ' IN (' . $ids . ')')
->where($db->qn('threat_score') . ' > ' . $db->q('0'))
->where($db->qn('acknowledged') . ' = ' . $db->q('0'))
->group($db->qn('scan_id'));
$suspiciousstats = $db->setQuery($query)->loadObjectList();
// Update the $resultArray with the loaded stats
if (!empty($alertstats))
{
foreach ($alertstats as $stat)
{
$idx = $map[$stat->scan_id];
if ($stat->newfile)
{
$resultArray[$idx]->files_new = $stat->count;
}
else
{
$resultArray[$idx]->files_modified = $stat->count;
}
}
}
if (!empty($suspiciousstats))
{
foreach ($suspiciousstats as $stat)
{
$idx = $map[$stat->scan_id];
$resultArray[$idx]->files_suspicious = $stat->count;
}
}
}
protected function onAfterDelete($id)
{
$this->deleteScanAlerts($id);
}
protected function onBeforeSave(&$data)
{
// Let's remove all the fields created on the fly
$fakeFields = ['newfile', 'files_new', 'files_modified', 'files_suspicious'];
foreach ($fakeFields as $field)
{
// I can't use `isset` since if we have a null key the check will return false, but that
// would cause an error during the update
if (array_key_exists($field, $this->recordData))
{
unset($this->recordData[$field]);
}
}
}
private function deleteScanAlerts($scan_id)
{
$db = $this->getDbo();
$query = $db->getQuery(true)
->delete('#__admintools_scanalerts')
->where($db->qn('scan_id') . ' = ' . $db->q($scan_id));
$db->setQuery($query)->execute();
return true;
}
private function postProcessStatusArray(array $statusArray, Logger $logger)
{
// Get the current scan record
$session = Session::getInstance();
$configuration = Configuration::getInstance();
$scanID = $session->get('scanID');
$scanRecord = $this->tmpInstance()->findOrFail($scanID);
$currentTime = new Date();
$warnings = $logger->getAndResetWarnings();
// Apply common updates to the backup record
$scanRecord->bind([
'totalfiles' => $session->get('scannedFiles'),
'scanend' => $currentTime->toSql(),
]);
// More work to do
if ($statusArray['HasRun'] && (empty($statusArray['Error'])))
{
$logger->debug('** More work necessary. Will resume in the next step.');
$scanRecord->save([
'status' => 'run',
]);
// Still have work to do
return [
'status' => true,
'done' => false,
'error' => '',
'warnings' => $warnings,
];
}
// An error occurred
if (!empty($statusArray['Error']))
{
$logger->debug('** An error occurred. The scan has died.');
$scanRecord->save([
'status' => 'fail',
]);
$session->reset();
return [
'status' => false,
'done' => true,
'error' => $statusArray['Error'],
'warnings' => $warnings,
];
}
// Just finished
// -- Send emails, if necessary
if ($scanRecord->origin != 'backend')
{
$logger->debug('Finished scanning. Evaluating whether to send email with scan results.');
$email = new Email($configuration, $session, $logger);
$email->sendEmail();
}
$logger->debug('** This scan is now finished.');
$scanRecord->save([
'status' => 'complete',
]);
$session->reset();
return [
'status' => true,
'done' => true,
'error' => '',
'warnings' => $warnings,
];
}
/**
* @param Configuration $configuration
* @param Session $session
* @param Logger $logger
* @param Timer $timer
* @param bool $enforceMinimumExecutionTime
*
* @return array
*
* @since 5.4.0
*/
private function tickScannerEngine(Configuration $configuration, Session $session, Logger $logger, Timer $timer, $enforceMinimumExecutionTime = true)
{
// Get the crawler and step it while we have enough time left
$crawler = new Crawler($configuration, $session, $logger, $timer);
$step = $session->get('step', 0);
$operation = 0;
$logger->debug(sprintf('===== Starting Step #%u =====', ++$step));
while (true)
{
$logger->debug(sprintf('----- Starting operation #%u -----', ++$operation));
$statusArray = $crawler->tick();
$logger->debug(sprintf('----- Finished operation #%u -----', $operation));
// Did we run into an error?
if ($crawler->getState() == Part::STATE_ERROR)
{
$logger->debug('The scanner engine has experienced an error.');
break;
}
// Are we done?
if ($crawler->getState() == Part::STATE_FINISHED)
{
$logger->debug('The scanner engine finished scanning your site.');
break;
}
// Did we run out of time?
if ($timer->getTimeLeft() <= 0)
{
$logger->debug('We are running out of time.');
break;
}
// Is the Break Flag set?
if ($session->get('breakFlag', false))
{
$logger->debug('The Break Flag is set.');
break;
}
}
$logger->debug(sprintf('===== Finished Step #%u =====', $step));
// Reset the break flag
$session->set('breakFlag', false);
// Do I need to enforce the minimum execution time?
if (!$enforceMinimumExecutionTime)
{
return $statusArray;
}
$minExec = $configuration->get('minExec');
$alreadyRun = $timer->getRunningTime();
$waitTime = $alreadyRun - $minExec;
// Negative wait times mean that we shouldn't wait. Also, waiting for less than 10 msec is daft.
if ($waitTime <= 0.01)
{
return $statusArray;
}
if (!function_exists('time_nanosleep'))
{
usleep(1000000 * $waitTime);
return $statusArray;
}
$seconds = floor($waitTime);
$fractional = $waitTime - $seconds;
time_nanosleep($seconds, $fractional * 1000000000);
return $statusArray;
}
}