ai_upgrade_assistant-0.2.0-alpha2/src/Service/UpdateMonitorService.php
src/Service/UpdateMonitorService.php
<?php
namespace Drupal\ai_upgrade_assistant\Service;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Service for monitoring Drupal module updates and security advisories.
*
* This service:
* - Monitors drupal.org for new releases
* - Tracks security advisories
* - Prioritizes updates based on security risk and complexity
* - Maintains update schedules
*/
class UpdateMonitorService {
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The logger factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* The project analyzer service.
*
* @var \Drupal\ai_upgrade_assistant\Service\ProjectAnalyzer
*/
protected $projectAnalyzer;
/**
* Drupal.org API endpoints.
*/
const DRUPAL_UPDATES_ENDPOINT = 'https://updates.drupal.org/release-history';
const DRUPAL_SECURITY_FEED = 'https://www.drupal.org/security/rss.xml';
/**
* Update check interval in seconds (default: 6 hours).
*/
const UPDATE_CHECK_INTERVAL = 21600;
/**
* Constructs a new UpdateMonitorService.
*
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
* @param \Drupal\ai_upgrade_assistant\Service\ProjectAnalyzer $project_analyzer
* The project analyzer service.
*/
public function __construct(
ClientInterface $http_client,
ModuleHandlerInterface $module_handler,
ConfigFactoryInterface $config_factory,
StateInterface $state,
LoggerChannelFactoryInterface $logger_factory,
ProjectAnalyzer $project_analyzer
) {
$this->httpClient = $http_client;
$this->moduleHandler = $module_handler;
$this->configFactory = $config_factory;
$this->state = $state;
$this->loggerFactory = $logger_factory;
$this->projectAnalyzer = $project_analyzer;
}
/**
* Checks for available updates for all installed modules.
*
* @return array
* Array of modules with available updates, keyed by module name.
* Each module entry contains:
* - current_version: Current installed version
* - available_version: Latest available version
* - security_update: Boolean indicating if it's a security update
* - complexity: Estimated update complexity (low/medium/high)
* - last_checked: Timestamp of last check
*/
public function checkForUpdates() {
$updates = [];
$modules = $this->moduleHandler->getModuleList();
foreach ($modules as $name => $module) {
// Skip if checked recently
$last_check = $this->state->get("ai_upgrade_assistant.update_check.{$name}", 0);
if ((time() - $last_check) < self::UPDATE_CHECK_INTERVAL) {
continue;
}
try {
// Query drupal.org updates API
$response = $this->httpClient->get(self::DRUPAL_UPDATES_ENDPOINT . '/' . $name . '/current');
$data = new \SimpleXMLElement($response->getBody()->getContents());
if (isset($data->releases->release[0])) {
$latest = $data->releases->release[0];
$current_version = $this->getModuleVersion($name);
if (version_compare($latest->version, $current_version, '>')) {
// Analyze update complexity
$complexity = $this->analyzeUpdateComplexity($name, $current_version, $latest->version);
$updates[$name] = [
'current_version' => $current_version,
'available_version' => (string) $latest->version,
'security_update' => (bool) $latest->security,
'complexity' => $complexity,
'last_checked' => time(),
];
}
}
// Update last check timestamp
$this->state->set("ai_upgrade_assistant.update_check.{$name}", time());
}
catch (RequestException $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error(
'Failed to check updates for @module: @error',
['@module' => $name, '@error' => $e->getMessage()]
);
}
}
// Store update data
$this->state->set('ai_upgrade_assistant.available_updates', $updates);
return $updates;
}
/**
* Checks for security advisories.
*
* @return array
* Array of security advisories.
*/
public function checkSecurityAdvisories() {
try {
$response = $this->httpClient->get(self::DRUPAL_SECURITY_FEED);
$feed = new \SimpleXMLElement($response->getBody()->getContents());
$advisories = [];
foreach ($feed->channel->item as $item) {
// Parse advisory details from title and description
$advisory = $this->parseSecurityAdvisory($item);
if ($advisory) {
$advisories[] = $advisory;
}
}
// Store advisories
$this->state->set('ai_upgrade_assistant.security_advisories', $advisories);
return $advisories;
}
catch (RequestException $e) {
$this->loggerFactory->get('ai_upgrade_assistant')->error(
'Failed to fetch security advisories: @error',
['@error' => $e->getMessage()]
);
return [];
}
}
/**
* Analyzes the complexity of an update.
*
* @param string $module_name
* The name of the module.
* @param string $current_version
* Current version of the module.
* @param string $target_version
* Target version for update.
*
* @return string
* Complexity level: 'low', 'medium', or 'high'
*/
protected function analyzeUpdateComplexity($module_name, $current_version, $target_version) {
// Get module's usage of APIs and hooks
$analysis = $this->projectAnalyzer->analyzeModule($module_name);
// Major version change indicates high complexity
if ($this->isMajorVersionChange($current_version, $target_version)) {
return 'high';
}
// Count the number of deprecated API uses
$deprecated_count = isset($analysis['deprecated_apis']) ? count($analysis['deprecated_apis']) : 0;
// Basic complexity heuristic
if ($deprecated_count > 10) {
return 'high';
}
elseif ($deprecated_count > 5) {
return 'medium';
}
return 'low';
}
/**
* Parses a security advisory from RSS feed item.
*
* @param \SimpleXMLElement $item
* RSS feed item.
*
* @return array|null
* Parsed advisory or NULL if not relevant.
*/
protected function parseSecurityAdvisory($item) {
$title = (string) $item->title;
$description = (string) $item->description;
$link = (string) $item->link;
// Only process security advisories
if (strpos($title, 'SA-') === false) {
return NULL;
}
return [
'title' => $title,
'description' => $description,
'link' => $link,
'date' => strtotime($item->pubDate),
'severity' => $this->parseSeverity($description),
'affected_modules' => $this->parseAffectedModules($description),
];
}
/**
* Parses severity from advisory description.
*
* @param string $description
* Advisory description.
*
* @return string
* Severity level: 'critical', 'high', 'moderate', or 'low'
*/
protected function parseSeverity($description) {
$description = strtolower($description);
if (strpos($description, 'critical') !== false) {
return 'critical';
}
elseif (strpos($description, 'highly critical') !== false) {
return 'high';
}
elseif (strpos($description, 'moderately critical') !== false) {
return 'moderate';
}
return 'low';
}
/**
* Extracts affected modules from advisory description.
*
* @param string $description
* Advisory description.
*
* @return array
* Array of affected module names.
*/
protected function parseAffectedModules($description) {
$modules = [];
// Common patterns for module names in security advisories
$patterns = [
'/\b(?:module|project)\s+([a-z0-9_]+)\b/i',
'/\b([a-z0-9_]+)\.module\b/i',
];
foreach ($patterns as $pattern) {
if (preg_match_all($pattern, $description, $matches)) {
$modules = array_merge($modules, $matches[1]);
}
}
return array_unique($modules);
}
/**
* Gets the installed version of a module.
*
* @param string $module_name
* The name of the module.
*
* @return string
* The installed version.
*/
protected function getModuleVersion($module_name) {
$module_data = system_get_info('module', $module_name);
return $module_data['version'] ?? '0.0';
}
/**
* Checks if update is a major version change.
*
* @param string $current_version
* Current version string.
* @param string $target_version
* Target version string.
*
* @return bool
* TRUE if major version change.
*/
protected function isMajorVersionChange($current_version, $target_version) {
$current_parts = explode('.', $current_version);
$target_parts = explode('.', $target_version);
return isset($current_parts[0]) && isset($target_parts[0]) &&
$current_parts[0] !== $target_parts[0];
}
}
