sgd_dashboard-1.0.0-beta1/src/Plugin/SgdCompanion/SgdCompanionSgdCore.php
src/Plugin/SgdCompanion/SgdCompanionSgdCore.php
<?php
namespace Drupal\sgd_dashboard\Plugin\SgdCompanion;
use Drupal\sgd_dashboard\SgdCompanionPluginBase;
/**
* Provides a SGD Companion plugin.
*
* @SgdCompanion(
* id = "sgd_companion_sgd_core",
* )
*/
class SgdCompanionSgdCore extends SgdCompanionPluginBase {
/**
* {@inheritdoc}
*
* The core companion can allways process something.
*/
public function canProcessStatus($statusData) : bool {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function saveStatus($websiteData, $statusData, $enabledProjects = NULL) : bool {
if ($this->canProcessStatus($statusData)) {
// The following values are provided by the Site Guardian API module.
$coreData = [
'drupal_version' => $statusData['drupal']['value'] ?? NULL,
'core_update_status' => strip_tags($statusData['update_core']['value'] ?? ''),
'contrib_update_status' => strip_tags($statusData['update_contrib']['value'] ?? ''),
'update_access' => $statusData['update access']['value'] ?? NULL,
'configuration_files' => $statusData['configuration_files']['value'] ?? NULL,
'file_system' => strip_tags($statusData['file system']['value'] ?? ''),
'cron_last_run' => $statusData['cron']['value'] ?? NULL,
'trusted_host_patterns' => $statusData['trusted_host_patterns']['value'] ?? NULL,
'php_version' => strtok($statusData['php']['value'] ?? '', '('),
'php_memory_limit' => $statusData['php_memory_limit']['value'] ?? NULL,
'php_apcu_caching' => $statusData['php_apcu_enabled']['value'] ?? NULL,
'php_opcache' => $statusData['php_opcache']['value'] ?? NULL,
'db_type' => $statusData['database_system']['value'] ?? NULL,
'db_version' => $statusData['database_system_version']['value'] ?? NULL,
'db_updates' => $statusData['update']['value'] ?? NULL,
'db_transaction_level' => $statusData['mysql_transaction_level']['value'] ?? NULL,
'webserver' => $statusData['webserver']['value'] ?? NULL,
'redis' => strip_tags($statusData['redis']['value'] ?? ''),
'sg_notes' => $statusData['sgd_api_notes']['value'] ?? NULL,
'updates_last_checked' => $statusData['sgd_updates_last_checked']['value'] ?? NULL,
'twig_debug_enabled' => $statusData['twig_debug_enabled']['value'] ?? NULL,
'render_cache_disabled' => $statusData['render_cache_disabled']['value'] ?? NULL,
];
// If we have uninstalled module data from the Site Guardian API module
// (1.0.6 onwards maybe).
if (!empty($statusData['sgd_projects_uninstalled_count'])) {
$coreData['projects_uninstalled'] = $statusData['sgd_projects_uninstalled_count']['value'];
}
// We need some module summary information displayed on the main website
// page.
// All of the following are derived from the enabled projects array.
// Total number of modules.
$moduleCount = count(array_filter($enabledProjects, function ($v) {
return $v['project_type'] == 'module';
}));
// Total number of themes.
$themeCount = count(array_filter($enabledProjects, function ($v) {
return $v['project_type'] == 'theme';
}));
// Projects with security updates.
$securityCount = count(array_filter($enabledProjects, function ($v) {
return ($v['name'] != 'drupal' && $v['status'] == '1');
}));
// Projects unsupported.
$unsupportedCount = count(array_filter($enabledProjects, function ($v) {
return ($v['name'] != 'drupal' && ($v['status'] == '2' || $v['status'] == '3'));
}));
// Projects with available updates.
$updatesCount = count(array_filter($enabledProjects, function ($v) {
return ($v['name'] != 'drupal' && $v['status'] == '4');
}));
$coreData += [
'enabled_modules_count' => $moduleCount,
'enabled_themes_count' => $themeCount,
'projects_security_count' => $securityCount,
'projects_unsupported_count' => $unsupportedCount,
'projects_updates_count' => $updatesCount,
];
// Serialize the status data and save the field.
$websiteData->set('data_core', serialize($coreData));
// Any values derived from the status data (or project data) that we want
// to use in views need to be stored in fields on the website data entity
// so Views can use them.
$websiteData->set('drupal_version', $this->getValueOrDefault($statusData['drupal'] ?? NULL));
$websiteData->set('php_version', strtok($this->getValueOrDefault($statusData['php'] ?? NULL), '('));
$websiteData->set('db_version', $this->getValueOrDefault($statusData['database_system_version'] ?? NULL));
// Is core version secure?
$coreIsSecure = $enabledProjects['drupal']['status'] != 1;
$websiteData->set('core_secure', $coreIsSecure);
// Are all contrib module versions secure?
$contribIsSecure = TRUE;
foreach ($enabledProjects as $moduleName => $moduleData) {
if (!in_array($moduleName, ['drupal'])) {
if ($moduleData['status'] == 1) {
$contribIsSecure = FALSE;
break;
}
}
}
$websiteData->set('contrib_secure', $contribIsSecure);
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getStatusDefaults() : array {
$data = [
'drupal' => [
'sg_notes' => [
'title' => $this->t('Site notes'),
'value' => 'n/a',
],
'drupal_version' => [
'title' => $this->t('Drupal version'),
'value' => 'n/a',
],
'core_update_status' => [
'title' => $this->t('Core update status'),
'value' => 'n/a',
],
'contrib_update_status' => [
'title' => $this->t('Contrib update status'),
'value' => 'n/a',
],
'db_updates' => [
'title' => $this->t('Database updates'),
'value' => 'n/a',
],
'cron_last_run' => [
'title' => $this->t('Cron last run'),
'value' => 0,
],
'configuration_files' => [
'title' => $this->t('Configuration files'),
'value' => 'n/a',
],
'update_access' => [
'title' => $this->t('Update access'),
'value' => 'n/a',
],
'file_system' => [
'title' => $this->t('File system writable'),
'value' => 'n/a',
],
'trusted_host_patterns' => [
'title' => $this->t('Trusted host patterns'),
'value' => 'n/a',
],
'twig_debug_enabled' => [
'title' => $this->t('Twig development mode'),
'value' => 'n/a',
],
'render_cache_disabled' => [
'title' => $this->t('Markup caching'),
'value' => 'n/a',
],
'available_updates_last_retreived' => [
'title' => $this->t('Updates last retrieved'),
'value' => 'n/a',
],
],
'hosting' => [
'webserver' => [
'title' => $this->t('Webserver'),
'value' => 'n/a',
],
'db_type' => [
'title' => $this->t('Database type'),
'value' => 'n/a',
],
'db_version' => [
'title' => $this->t('Database version'),
'value' => 'n/a',
],
'db_transaction_level' => [
'title' => $this->t('Database transaction level'),
'value' => 'n/a',
],
'redis' => [
'title' => $this->t('Redis'),
'value' => 'n/a',
],
],
'projects' => [
'enabled_modules_count' => [
'title' => $this->t('Enabled projects count'),
'value' => 'n/a',
],
'enabled_themes_count' => [
'title' => $this->t('Enabled themes count'),
'value' => 'n/a',
],
'updates_available_count' => [
'title' => $this->t('Projects with updates available'),
'value' => 'n/a',
],
'uninstalled' => [
'title' => $this->t('Projects with no enabled modules'),
'value' => 'n/a',
],
'security_count' => [
'title' => $this->t('Projects with security updates'),
'value' => 'n/a',
],
'unsupported_count' => [
'title' => $this->t('Projects no longer supported'),
'value' => 'n/a',
],
],
'php' => [
'php_version' => [
'title' => $this->t('PHP version'),
'value' => 'n/a',
],
'php_memory_limit' => [
'title' => $this->t('PHP memory limit'),
'value' => 'n/a',
],
'php_apcu_caching' => [
'title' => $this->t('PHP APCu caching'),
'value' => 'n/a',
],
'php_opcache' => [
'title' => $this->t('PHP opcache'),
'value' => 'n/a',
],
],
];
return $data;
}
/**
* {@inheritdoc}
*/
public function getStatus($websiteData) : array | NULL {
// Get the titles and value defaults so we can display something sensible
// even if the website has not been queried for the core data yet.
$status = $this->getStatusDefaults();
// If we have core data from the website.
if ($dataSerialized = $websiteData->get('data_core')->value) {
$data = unserialize($dataSerialized, ['allowed_classes' => FALSE]);
// Update the values in the status with the data returned from the API
// Here is the oppertunity to massage/transform any data we got from the
// website status data.
// Do it here rather than when saving so we retain all info received as
// it's received.
// If no updates last checked date then set to 0 so twig date output
// doesnt complain.
$data['updates_last_checked'] = $data['updates_last_checked'] ?? 0;
// Redis is returned as an empty string if not connected so update with
// the default if it is blank.
$data['redis'] = !empty($data['redis']) ? $data['redis'] : $status['hosting']['redis']['value'];
// If no projects uninstalled value then use default.
$data['projects_uninstalled'] = $data['projects_uninstalled'] ?? $status['projects']['uninstalled']['value'];
// twig_debug_enabled and render_cache_disabled only introduced in
// D10.1.0 - Will version_compare always be ok for Drupal version????
if (version_compare($data['drupal_version'], '10.1.0', '>=')) {
$twigDebug = !empty($data['twig_debug_enabled']) ? $this->t('Enabled') : $this->t('Disabled');
$rcDisabled = empty($data['render_cache_disabled']) ? $this->t('Enabled') : $this->t('Disabled');
}
else {
$twigDebug = $this->t("Not available prior to Drupal 10.1.");
$rcDisabled = $this->t("Not available prior to Drupal 10.1.");
}
// Format the updates last checked date into something reaosnable.
$availableUpdatesLastChecked = $this->dateFormatter->format($data['updates_last_checked'], 'custom', 'l jS \of F Y \a\t H:i:s');
// Merge the data with the titles and defaults.
$status = array_replace_recursive($status, [
'drupal' => [
'sg_notes' => [
'value' => $data['sg_notes'],
],
'drupal_version' => [
'value' => $data['drupal_version'],
],
'core_update_status' => [
'value' => $data['core_update_status'],
],
'contrib_update_status' => [
'value' => $data['contrib_update_status'],
],
'db_updates' => [
'value' => $data['db_updates'],
],
'cron_last_run' => [
'value' => $data['cron_last_run'],
],
'configuration_files' => [
'value' => $data['configuration_files'],
],
'update_access' => [
'value' => $data['update_access'],
],
'file_system' => [
'value' => $data['file_system'],
],
'trusted_host_patterns' => [
'value' => $data['trusted_host_patterns'],
],
'available_updates_last_retreived' => [
'value' => $availableUpdatesLastChecked,
],
'twig_debug_enabled' => [
'value' => $twigDebug,
],
'render_cache_disabled' => [
'value' => $rcDisabled,
],
],
'hosting' => [
'webserver' => [
'value' => $data['webserver'],
],
'db_type' => [
'value' => $data['db_type'],
],
'db_version' => [
'value' => $data['db_version'],
],
'db_transaction_level' => [
'value' => $data['db_transaction_level'],
],
'redis' => [
'value' => $data['redis'],
],
],
'projects' => [
'enabled_modules_count' => [
'value' => $data['enabled_modules_count'],
],
'enabled_themes_count' => [
'value' => $data['enabled_themes_count'],
],
'updates_available_count' => [
'value' => $data['projects_updates_count'],
],
'uninstalled' => [
'value' => $data['projects_uninstalled'],
],
'security_count' => [
'value' => $data['projects_security_count'],
],
'unsupported_count' => [
'value' => $data['projects_unsupported_count'],
],
],
'php' => [
'php_version' => [
'value' => $data['php_version'],
],
'php_memory_limit' => [
'value' => $data['php_memory_limit'],
],
'php_apcu_caching' => [
'value' => $data['php_apcu_caching'],
],
'php_opcache' => [
'value' => $data['php_opcache'],
],
],
]);
}
return $status;
}
/**
* {@inheritdoc}
*/
public function getBuildElements($websiteData) : array | NULL {
if ($data = $this->getStatus($websiteData)) {
// Validate the values and add validation status info so can be
// displayed.
$validation = $this->validate($data['drupal']);
foreach ($data['drupal'] as $key => $value) {
if (!empty($validation[$key])) {
$data['drupal'][$key]['status'] = $validation[$key];
}
}
$validation = $this->validate($data['hosting']);
foreach ($data['hosting'] as $key => $value) {
if (!empty($validation[$key])) {
$data['hosting'][$key]['status'] = $validation[$key];
}
}
}
return $data;
}
/**
* Validate data.
*
* Checks each item and returns a good/neutral/bad status that can be
* displayed on page or in a report.
*/
private function validate(&$statusData): array {
$validation = [];
// Validate core variables
// Each validation is hard coded as it differs for each plugin.
// You dont have to validate everything. Just things that it makes sense
// to.
foreach ($statusData as $key => $status) {
switch ($key) {
case 'core_update_status':
case 'contrib_update_status':
case 'db_updates':
if (str_starts_with($status['value'], 'Not secure!')) {
$validation[$key] = [
'class' => 'error',
'text' => $this->t('Issue'),
'message' => $this->t('@var - Not secure. Review and update immediately.', ['@var' => $status['title']]),
];
}
elseif ($status['value'] != 'Up to date') {
$validation[$key] = [
'class' => 'warning',
'text' => $this->t('Alert'),
'message' => $this->t('@var - Not up-to-date. Review and update as required.', ['@var' => $status['title']]),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'configuration_files':
if ($status['value'] == 'Protection disabled') {
$validation[$key] = [
'class' => 'error',
'text' => $this->t('Issue'),
'message' => $this->t('Configuration files are not protected.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'file_system_writable':
$lastRunString = substr($status['value'], strlen('Last run '));
if (new \DateTime($lastRunString) < new \DateTime('1 day ago')) {
$validation[$key] = [
'class' => 'error',
'text' => $this->t('Issue'),
'message' => $this->t('CRON has not run for more than a day.'),
];
}
break;
case 'trusted_host_patterns':
if ($status['value'] != 'Enabled') {
$validation[$key] = [
'class' => 'error',
'text' => $this->t('Issue'),
'message' => $this->t('No trusted host pattern has been specified which makes the site vulnerable to HTTP Host header spoofing.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'redis':
if (!str_starts_with($status['value'], 'Connected')) {
$validation[$key] = [
'class' => 'warning',
'text' => $this->t('Alert'),
'message' => $this->t('No REDIS cache server is connected. Drupal performance is significantly enhanced with an in memory cache.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'cron_last_run':
$lastRunString = substr($status['value'], strlen('Last run '));
if (new \DateTime($lastRunString) < new \DateTime('1 day ago')) {
$validation[$key] = [
'class' => 'error',
'text' => $this->t('Issue'),
'message' => $this->t('CRON has not run for more than a day.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'update_access':
if ($status['value'] == 'Not protected') {
$validation[$key] = [
'class' => 'error',
'text' => $this->t('Issue'),
'message' => $this->t('The update.php script is accessible to everyone without authentication, which is a security risk.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'db_transaction_level':
if ($status['value'] != 'READ-COMMITTED') {
$validation[$key] = [
'class' => 'warning',
'text' => $this->t('Alert'),
'message' => $this->t('Recommended value for DB transation level is READ-COMMITTED.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
break;
case 'twig_debug_enabled':
if (version_compare($statusData['drupal_version']['value'], '10.1.0', '>=')) {
if ($status['value'] == 'Enabled') {
$validation[$key] = [
'class' => 'warning',
'text' => $this->t('Alert'),
'message' => $this->t('Twig debug is enabled.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
}
// If Drupal version is prior to 10.1 then we dont know the value as
// it doesn't really exist.
else {
$statusData[$key]['value'] = $this->t('n/a');
$validation[$key] = [
'message' => $this->t('Not available prior to Drupal 10.1.'),
];
}
break;
case 'render_cache_disabled':
if (version_compare($statusData['drupal_version']['value'], '10.1.0', '>=')) {
if ($status['value'] == 'Disabled') {
$validation[$key] = [
'class' => 'warning',
'text' => $this->t('Alert'),
'message' => $this->t('Render cache, dynamic page cache, and page cache are bypassed.'),
];
}
else {
$validation[$key] = [
'class' => 'ok',
'text' => $this->t('OK'),
];
}
}
// If Drupal version is prior to 10.1 then we dont know the value as
// it doesn't really exist.
else {
$statusData[$key]['value'] = $this->t('n/a');
$validation[$key] = [
'message' => $this->t('Not available prior to Drupal 10.1.'),
];
}
break;
case 'available_updates_last_retreived':
$validation[$key] = [
'message' => $this->t('The last time the website requested update information from Drupal.org. This data is cached by the website and only requested periodically.'),
];
break;
}
}
return $validation;
}
}
