View source
<?php
namespace Drupal\cms_content_sync_health\Controller;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\PullIntent;
use EdgeBox\SyncCore\Interfaces\IReportingService;
use EdgeBox\SyncCore\V1\Helper;
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\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
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 GuzzleHttp\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;
use function t;
class SyncHealth extends ControllerBase {
protected $database;
protected $moduleHandler;
protected $configFactory;
protected $dateFormatter;
protected $httpClient;
protected $messenger;
protected $entityTypeManager;
public function __construct(Connection $database, ModuleHandler $moduleHandler, ConfigFactory $configFactory, DateFormatter $dateFormatter, Client $httpClient, MessengerInterface $messenger, EntityTypeManager $entityTypeManager) {
$this->database = $database;
$this->moduleHandler = $moduleHandler;
$this->configFactory = $configFactory;
$this->dateFormatter = $dateFormatter;
$this->httpClient = $httpClient;
$this->messenger = $messenger;
$this->entityTypeManager = $entityTypeManager;
}
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'));
}
protected static function formatMessage($row) {
if (isset($row->message, $row->variables)) {
$variables = @unserialize($row->variables);
if ($variables === NULL) {
$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),
]);
}
else {
$message = t(Xss::filterAdmin($row->message), $variables);
}
}
else {
$message = FALSE;
}
return $message;
}
protected function countStatusEntitiesWithFlag($flag, $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;
}
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;
}
$result = Helper::obfuscateCredentials($result);
return $result;
}
protected function filterSyncCoreLogMessages($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);
}
public function overview() {
$sync_cores = [];
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;
}
$module_info = \Drupal::service('extension.list.module')
->getExtensionInfo('cms_content_sync');
$moduleHandler = $this->moduleHandler;
if ($moduleHandler
->moduleExists('update')) {
$updates = new UpdateFetcher($this->configFactory, $this->httpClient);
$available = $updates
->fetchProjectData([
'name' => 'cms_content_sync',
'info' => $module_info,
'includes' => [],
'project_type' => 'module',
'project_status' => TRUE,
]);
preg_match_all('@<version>\\s*8.x-([0-9]+)\\.([0-9]+)\\s*</version>@i', $available, $versions, PREG_SET_ORDER);
$newest_major = 0;
$newest_minor = 0;
foreach ($versions as $version) {
if ($version[1] > $newest_major) {
$newest_major = $version[1];
$newest_minor = $version[2];
}
elseif ($version[1] == $newest_major && $version[2] > $newest_minor) {
$newest_minor = $version[2];
}
}
$newest_version = $newest_major . '.' . $newest_minor;
}
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(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(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(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,
];
}
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 ($config['export'] != PushIntent::PUSH_AUTOMATICALLY) {
continue;
}
if (!in_array(Pool::POOL_USAGE_FORCE, array_values($config['export_pools']))) {
continue;
}
$checked[] = $id;
$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');
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;
}
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;
}
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;
}
}
}
$count = $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();
return $count;
}
protected function countEntitiesWaitingForPush() {
return 0;
}
public function pushing() {
$push_failures_hard = $this
->countStatusEntitiesWithFlag(EntityStatus::FLAG_PUSH_FAILED, [
'request_failed' => PushIntent::PUSH_FAILED_REQUEST_FAILED,
'invalid_status_code' => PushIntent::PUSH_FAILED_REQUEST_INVALID_STATUS_CODE,
'dependency_push_failed' => PushIntent::PUSH_FAILED_DEPENDENCY_PUSH_FAILED,
'internal_error' => PushIntent::PUSH_FAILED_INTERNAL_ERROR,
]);
$push_failures_soft = $this
->countStatusEntitiesWithFlag(EntityStatus::FLAG_PUSH_FAILED_SOFT, [
'handler_denied' => PushIntent::PUSH_FAILED_HANDLER_DENIED,
'unchanged' => PushIntent::PUSH_FAILED_UNCHANGED,
]);
$pending = [
'stale_entities' => $this
->countStaleEntities(),
'version_changed' => $this
->countEntitiesWithChangedVersionForPush(),
'manual_push' => $this
->countEntitiesWaitingForPush(),
];
return [
'#theme' => 'cms_content_sync_sync_health_push',
'#push_failures_hard' => $push_failures_hard,
'#push_failures_soft' => $push_failures_soft,
'#pending' => $pending,
];
}
public function pulling() {
$pull_failures_hard = $this
->countStatusEntitiesWithFlag(EntityStatus::FLAG_PULL_FAILED, [
'different_version' => PullIntent::PULL_FAILED_DIFFERENT_VERSION,
'sync_error' => PullIntent::PULL_FAILED_CONTENT_SYNC_ERROR,
'internal_error' => PullIntent::PULL_FAILED_INTERNAL_ERROR,
]);
$pull_failures_soft = $this
->countStatusEntitiesWithFlag(EntityStatus::FLAG_PULL_FAILED_SOFT, [
'handler_denied' => PullIntent::PULL_FAILED_HANDLER_DENIED,
'no_flow' => PullIntent::PULL_FAILED_NO_FLOW,
'unknown_pool' => PullIntent::PULL_FAILED_UNKNOWN_POOL,
]);
return [
'#theme' => 'cms_content_sync_sync_health_pull',
'#pull_failures_hard' => $pull_failures_hard,
'#pull_failures_soft' => $pull_failures_soft,
];
}
}