cms_content_sync-3.0.x-dev/modules/cms_content_sync_health/src/Controller/SyncHealth.php
modules/cms_content_sync_health/src/Controller/SyncHealth.php
<?php
namespace Drupal\cms_content_sync_health\Controller;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncCoreInterface\DrupalApplication;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\update\UpdateFetcher;
use EdgeBox\SyncCore\Interfaces\IReportingService;
use GuzzleHttp\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;
use function t;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Site\Settings;
use EdgeBox\SyncCore\V2\Helper;
/**
* Provides a listing of Flow.
*/
class SyncHealth extends ControllerBase {
/**
* The Drupal core database connection.
*
* @var \Drupal\Core\Database\Database
*/
protected $database;
/**
* The Drupal Core module handler.
*
* @var \Drupal\Core\Extension\ModuleHandler
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $configFactory;
/**
* {@inheritdoc}
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* {@inheritdoc}
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* @var \Drupal\Core\Site\Settings
*/
protected $settings;
/**
* {@inheritdoc}
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* {@inheritdoc}
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* Constructs a \Drupal\cms_content_sync_health\Controller\SyncHealth object.
*/
public function __construct(Connection $database, ModuleHandler $moduleHandler, ConfigFactory $configFactory, DateFormatter $dateFormatter, Client $httpClient, MessengerInterface $messenger, EntityTypeManager $entityTypeManager, ModuleExtensionList $moduleExtensionList, Settings $settings) {
$this->database = $database;
$this->moduleHandler = $moduleHandler;
$this->configFactory = $configFactory;
$this->dateFormatter = $dateFormatter;
$this->httpClient = $httpClient;
$this->messenger = $messenger;
$this->entityTypeManager = $entityTypeManager;
$this->moduleExtensionList = $moduleExtensionList;
$this->settings = $settings;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('module_handler'),
$container->get('config.factory'),
$container->get('date.formatter'),
$container->get('http_client'),
$container->get('messenger'),
$container->get('entity_type.manager'),
$container->get('extension.list.module'),
$container->get('settings')
);
}
/**
* Render the overview page.
*
* @return array
* Returns the overview page render array.
*/
public function overview() {
$sync_cores = [];
if (DrupalApplication::get()->getSiteUuid()) {
foreach (SyncCoreFactory::getAllSyncCores() as $host => $core) {
$status = $core->getReportingService()->getStatus();
$reporting = $core->getReportingService();
$status['error_log'] = $this->filterSyncCoreLogMessages($reporting->getLog(IReportingService::LOG_LEVEL_ERROR));
$status['warning_log'] = $this->filterSyncCoreLogMessages($reporting->getLog(IReportingService::LOG_LEVEL_WARNING));
$sync_cores[$host] = $status;
}
}
else {
\Drupal::messenger()->addWarning($this->t('Site is not registered.'));
}
$module_info = $this->moduleExtensionList->getExtensionInfo('cms_content_sync');
$moduleHandler = $this->moduleHandler;
if ($moduleHandler->moduleExists('update')) {
$updates = new UpdateFetcher($this->configFactory, $this->httpClient, $this->settings);
$available = $updates->fetchProjectData([
'name' => 'cms_content_sync',
'info' => $module_info,
'includes' => [],
'project_type' => 'module',
'project_status' => TRUE,
]);
if ($available) {
preg_match_all('@<version>\s*([0-9]+)\.([0-9]+)\.([0-9]+)\s*</version>@i', $available, $versions, PREG_SET_ORDER);
}
else {
$versions = [];
}
$newest_major = 0;
$newest_minor = 0;
$newest_build = 0;
foreach ($versions as $version) {
if ($version[1] > $newest_major) {
$newest_major = $version[1];
$newest_minor = $version[2];
$newest_build = $version[3];
}
elseif ($version[1] == $newest_major) {
if ($version[2] > $newest_minor) {
$newest_minor = $version[2];
$newest_build = $version[3];
}
elseif ($version[2] == $newest_minor) {
if ($version[3] > $newest_build) {
$newest_build = $version[3];
}
}
}
}
$newest_version = $newest_major . '.' . $newest_minor . '.' . $newest_build;
}
else {
$newest_version = NULL;
}
if (isset($module_info['version'])) {
$module_version = $module_info['version'];
$module_version = preg_replace('@^\d\.x-(.*)$@', '$1', $module_version);
if ($module_version != $newest_version) {
if ($newest_version) {
$this->messenger->addMessage($this->t("There's an update available! The newest module version is @newest, yours is @current.", [
'@newest' => $newest_version,
'@current' => $module_version,
]));
}
else {
$this->messenger->addMessage($this->t("Please enable the 'update' module to see if you're running the latest Content Sync version."));
}
}
}
else {
$module_version = NULL;
if ($newest_version) {
$this->messenger->addWarning($this->t("You're running a dev release. The newest module version is @newest.", [
'@newest' => $newest_version,
]));
}
}
$push_failures_hard = $this->countStatusEntitiesWithFlag(EntityStatus::FLAG_PUSH_FAILED);
$push_failures_soft = $this->countStatusEntitiesWithFlag(EntityStatus::FLAG_PUSH_FAILED_SOFT);
$pull_failures_hard = $this->countStatusEntitiesWithFlag(EntityStatus::FLAG_PULL_FAILED);
$pull_failures_soft = $this->countStatusEntitiesWithFlag(EntityStatus::FLAG_PULL_FAILED_SOFT);
$version_differences['local'] = $this->getLocalVersionDifferences();
$moduleHandler = $this->moduleHandler;
$dblog_enabled = $moduleHandler->moduleExists('dblog');
if ($dblog_enabled) {
$site_log_disabled = FALSE;
$error_log = $this->getLocalLogMessages([
RfcLogLevel::EMERGENCY,
RfcLogLevel::ALERT,
RfcLogLevel::CRITICAL,
RfcLogLevel::ERROR,
]);
$warning_log = $this->getLocalLogMessages([
RfcLogLevel::WARNING,
]);
}
else {
$site_log_disabled = TRUE;
$error_log = NULL;
$warning_log = NULL;
}
return [
'#theme' => 'cms_content_sync_sync_health_overview',
'#sync_cores' => $sync_cores,
'#module_version' => $module_version,
'#newest_version' => $newest_version,
'#push_failures_hard' => $push_failures_hard,
'#push_failures_soft' => $push_failures_soft,
'#pull_failures_hard' => $pull_failures_hard,
'#pull_failures_soft' => $pull_failures_soft,
'#version_differences' => $version_differences,
'#error_log' => $error_log,
'#warning_log' => $warning_log,
'#site_log_disabled' => $site_log_disabled,
];
}
/**
* Formats a database log message.
*
* @param object $row
* The record from the watchdog table. The object properties are: wid, uid,
* severity, type, timestamp, message, variables, link, name.
*
* @return false|string|TranslatableMarkup
* The formatted log message or FALSE if the message or variables properties
* are not set
*/
protected static function formatMessage($row) {
// Check for required properties.
if (isset($row->message, $row->variables)) {
$variables = @unserialize($row->variables);
// Messages without variables or user specified text.
if (NULL === $variables) {
$message = Xss::filterAdmin($row->message);
}
elseif (!is_array($variables)) {
$message = t('Log data is corrupted and cannot be unserialized: @message', ['@message' => Xss::filterAdmin($row->message)]);
}
// Message to translate with injected variables.
else {
// @codingStandardsIgnoreStart
$message = t(Xss::filterAdmin($row->message), $variables);
// @codingStandardsIgnoreEnd
}
}
else {
$message = FALSE;
}
return $message;
}
/**
* Count status entities with the given flag.
*
* @param int $flag
* See EntityStatus::FLAG_*.
* @param array $details
* Search the 'data' column to contain the given $value and save it in the
* result array at $key.
*
* @return array
* The counts, always having 'total'=>... and optionally the counts given
* by $details.
*/
protected function countStatusEntitiesWithFlag($flag, array $details = []) {
$result['total'] = $this->database->select('cms_content_sync_entity_status')
->where('flags&:flag=:flag', [':flag' => $flag])
->countQuery()
->execute()
->fetchField();
if ($result['total']) {
foreach ($details as $name => $search) {
$search = '%' . $this->database->escapeLike($search) . '%';
$result[$name] = $this->database->select('cms_content_sync_entity_status')
->where('flags&:flag=:flag', [':flag' => $flag])
->condition('data', $search, 'LIKE')
->countQuery()
->execute()
->fetchField();
}
}
return $result;
}
/**
* Get content sync related log messages.
*
* @param mixed $levels
* The depth to be returned.
* @param mixed $count
* The amount of log messages to be returned.
*/
protected function getLocalLogMessages($levels, $count = 10) {
$result = [];
$connection = $this->database;
$query = $connection
->select('watchdog', 'w')
->fields('w', ['timestamp', 'severity', 'message', 'variables'])
->orderBy('timestamp', 'DESC')
->range(0, $count)
->condition('type', 'cms_content_sync')
->condition('severity', $levels, 'IN');
$query = $query->execute();
$rows = $query->fetchAll();
foreach ($rows as $res) {
$message =
'<em>' .
$this->dateFormatter->format($res->timestamp, 'long') .
'</em> ' .
self::formatMessage($res)->render();
$result[] = $message;
}
return Helper::obfuscateCredentials($result);
}
/**
* Filter the given messages to only display those related to this site.
*
* @param array[] $messages
* The messages to filtered.
*
* @return array[]
* Returns the filtered log messages.
*/
protected function filterSyncCoreLogMessages(array $messages) {
$result = [];
$allowed_prefixes = [];
foreach (Pool::getAll() as $pool) {
$allowed_prefixes[] = 'drupal-' . $pool->id() . '-' . DrupalApplication::get()->getSiteMachineName() . '-';
}
foreach ($messages as $msg) {
if (!isset($msg['connection_id'])) {
continue;
}
$keep = FALSE;
foreach ($allowed_prefixes as $allowed) {
if (substr($msg['connection_id'], 0, strlen($allowed)) == $allowed) {
$keep = TRUE;
break;
}
}
if ($keep) {
$result[] = $msg;
}
}
return array_slice($result, -20);
}
/**
* Returns count of stale entities.
*/
protected function countStaleEntities() {
$checked = [];
$count = 0;
foreach (Flow::getAll() as $flow) {
foreach ($flow->getController()->getEntityTypeConfig(NULL, NULL, TRUE) as $type_name => $bundles) {
foreach ($bundles as $bundle_name => $config) {
$id = $type_name . "\n" . $bundle_name;
if (in_array($id, $checked)) {
continue;
}
if (PushIntent::PUSH_AUTOMATICALLY != $config['export']) {
continue;
}
if (!in_array(Pool::POOL_USAGE_FORCE, array_values($config['export_pools']))) {
continue;
}
$checked[] = $id;
/**
* @var \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
*/
$entityTypeManager = $this->entityTypeManager;
$type = $entityTypeManager->getDefinition($type_name);
$query = $this->database->select($type->getBaseTable(), 'e');
$query
->leftJoin('cms_content_sync_entity_status', 's', 'e.uuid=s.entity_uuid AND s.entity_type=:type', [':type' => $type_name]);
$query = $query
->isNull('s.id');
// Some entity types don't store their bundle information in their
// table if they don't actually have multiple bundles.
if (!in_array($type_name, ['bibcite_contributor', 'bibcite_keyword'])) {
$query = $query
->condition('e.' . $type->getKey('bundle'), $bundle_name);
}
$result = $query
->countQuery()
->execute();
$count += (int) $result
->fetchField();
}
}
}
return $count;
}
/**
* Returns the local version differences.
*/
protected function getLocalVersionDifferences() {
$result = [];
foreach (Flow::getAll() as $flow) {
foreach ($flow->getController()->getEntityTypeConfig(NULL, NULL, TRUE) as $type_name => $bundles) {
foreach ($bundles as $bundle_name => $config) {
$version = $config['version'];
$current = Flow::getEntityTypeVersion($type_name, $bundle_name);
if ($version == $current) {
continue;
}
$result[] = $flow->label() . ' uses entity type ' . $type_name . '.' . $bundle_name . ' with version ' . $version . '. Current version is ' . $current . '. Please update the Flow.';
}
}
}
return $result;
}
/**
* Returns the count of entities with a changed version for the push.
*/
protected function countEntitiesWithChangedVersionForPush() {
$checked = [];
$versions = [];
$types = [];
foreach (Flow::getAll() as $flow) {
foreach ($flow->getController()->getEntityTypeConfig(NULL, NULL, TRUE) as $type_name => $bundles) {
foreach ($bundles as $bundle_name => $config) {
$id = $type_name . "\n" . $bundle_name;
if (in_array($id, $checked)) {
continue;
}
$checked[] = $id;
$version = $config['version'];
if (!in_array($type_name, $types)) {
$types[] = $type_name;
}
$versions[] = $version;
}
}
}
return $this->database->select('cms_content_sync_entity_status')
->condition('entity_type', $types, 'IN')
->condition('entity_type_version', $versions, 'NOT IN')
->where('flags&:flag=:flag', [':flag' => EntityStatus::FLAG_IS_SOURCE_ENTITY])
->countQuery()
->execute()
->fetchField();
}
}
