autoupdate-8.x-1.x-dev/src/ModuleManager.php
src/ModuleManager.php
<?php
namespace Drupal\autoupdate;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\node\Entity\Node;
use Drupal\Core\Url;
use Drupal\autoupdate\ModuleManagerInterface;
use Drupal\Component\Utility\Crypt;
/**
* Provides methods for ModuleManager.
*/
class ModuleManager implements ModuleManagerInterface {
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Composer Lock data.
*/
protected $composerLock = NULL;
/**
* Composer Json data.
*/
protected $composerJson = NULL;
/**
* Drupal Core Version.
*/
protected $drupalCore = NULL;
/**
* Site key.
*/
protected $siteKey = NULL;
/**
* Creates a new ModuleManager manager.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) {
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->drupalCore = explode('.', \Drupal::VERSION)[0];
if (isset($_SERVER['SERVER_NAME'])) {
$server = $_SERVER['SERVER_NAME'];
}
else {
$server = getenv('HOME');
}
$this->siteKey = Crypt::hmacBase64($server, '');
}
/**
* Get all modules.
*
* @param bool $installed
*
* @return array
*/
public function getModules($installed = TRUE) {
// Get all modules.
$all_modules = $this->getModuleData();
$selected_modules = array();
foreach ($all_modules as $module) {
// Only installed.
if ($installed == TRUE) {
if ($module->status == 0) {
continue;
}
}
// Check core pathes.
if (strpos($module->getPathname(), 'core/') === 0) {
// Add core if dont exists.
if (!isset($selected_modules['core'])) {
// Add data for core.
$version = NULL;
$mtime = NULL;
$install_type = 'manual';
$composer_path = NULL;
$composer_setting = NULL;
$composer_data = $this->getComposerData('core');
$composer_data_recommended = $this->getComposerData('core-recommended');
$corename = 'core';
if ($composer_data['composer'] == TRUE) {
$install_type = 'composer';
$version = $composer_data['version'];
$mtime = $composer_data['time'];
$composer_path = $composer_data['composer_path'];
$composer_setting = $composer_data['composer_setting'];
}
if ($composer_data_recommended['composer'] == TRUE) {
$corename = 'core-recommended';
$install_type = 'composer';
$version = $composer_data_recommended['version'];
$mtime = $composer_data_recommended['time'];
$composer_path = 'drupal/core "drupal/core-*"';
$composer_setting = $composer_data_recommended['composer_setting'];
}
$selected_modules[$corename] = [
'name' => 'drupal',
'version' => $version,
'install_type' => $install_type,
'path' => $corename,
'composer_path' => $composer_path,
'composer_setting' => $composer_setting,
'submodule' => FALSE,
'time' => $mtime,
];
}
continue;
}
// Read data.
$name = $module->getName();
$path = str_replace('/' . $name . '.info.yml', '', $module->getPathname());
// Get version.
$version = NULL;
if (isset($module->info['version'])) {
$version = str_replace('x-', '', $module->info['version']);
}
// Get mtime.
$mtime = NULL;
if (isset($module->info['mtime'])) {
$mtime = $module->info['mtime'];
}
// Get composer data.
$install_type = 'manual';
$composer_path = NULL;
$composer_data = $this->getComposerData($name);
$composer_setting = NULL;
if ($composer_data['composer'] == TRUE) {
$install_type = 'composer';
$version = $composer_data['version'];
$mtime = $composer_data['time'];
$composer_path = $composer_data['composer_path'];
$composer_setting = $composer_data['composer_setting'];
}
$selected_modules[$name] = array(
'name' => $name,
'version' => $version,
'install_type' => $install_type,
'path' => $path,
'composer_path' => $composer_path,
'composer_setting' => $composer_setting,
'submodule' => FALSE,
'time' => $mtime,
);
}
// Search for submodules.
foreach ($selected_modules as $module) {
$module_name = $module['name'];
foreach ($selected_modules as $key_search => $module_search) {
// Ignore own entry.
if($module_search['name'] != $module_name) {
// Search if module exists in path.
if (strpos($module_search['path'], '/'. $module_name . '/') !== false) {
$selected_modules[$key_search]['submodule'] = TRUE;
}
}
}
}
// Remove submodules.
foreach ($selected_modules as $key => $module) {
// Ignore submodule.
if ($module['submodule'] == TRUE) {
unset($selected_modules[$key]);
}
unset($selected_modules[$key]['submodule']);
}
return $selected_modules;
}
/**
* Return all module updates and releases.
*
* @param bool $security_only
* @param bool $force_check
*
* @return array
*/
public function getUpdates($security_only = FALSE, $force_check = FALSE) {
// Get module settings.
$config = \Drupal::config('autoupdate.settings');
$check_inteval = ($config->get('check_interval') == '' ? 43200 : $config->get('check_interval'));
$email = $config->get('email');
// Get data from state.
$state_data = \Drupal::state()->get('autoupdate.updates', FALSE);
// Check interval time.
$send_notification = FALSE;
if (isset($state_data['generate'])) {
if ($state_data['generate'] + $check_inteval < time()) {
$force_check = TRUE;
$send_notification = TRUE;
}
}
// Renew module data.
if ($state_data == FALSE OR $force_check == TRUE) {
// Get installed modules.
$all_modules = $this->getModules(TRUE);
foreach ($all_modules as $key => $module) {
// Get release.
$all_modules[$key]['updates']['security'] = 0;
$all_modules[$key]['updates']['insecure'] = 0;
$all_modules[$key]['updates']['releases'] = array();
if (strpos($module['composer_path'], 'drupal/') === 0) {
$all_modules[$key]['updates'] = $this->getVersionHistory($module['name'], $module['version']);
}
}
// Save it to state.
$state_data = array('generate' => time(), 'data' => $all_modules);
\Drupal::state()->setMultiple(array('autoupdate.updates' => $state_data));
}
// Only security updates.
if ($security_only == TRUE) {
foreach ($state_data['data'] as $key => $module) {
if ($module['updates']['security'] == 1) {
continue;
}
if ($module['updates']['insecure'] == 1) {
continue;
}
unset($state_data['data'][$key]);
}
}
// Send notification.
if ($email != '' AND $send_notification == TRUE) {
// Check if security updates ar available.
$security_modules = array();
$security_available = FALSE;
foreach ($state_data['data'] as $key => $module) {
if ($module['updates']['security'] == 1 OR $module['updates']['insecure'] == 1) {
$security_available = TRUE;
$module_text = $module['name'];
// Check if module has fixed version.
if (strpos($module['composer_setting'], '^') === false AND strpos($module['composer_setting'], '*') === false) {
// Set info text.
$module_text = $module['name'] . ' (cannot update it, you fixed the version with composer)';
}
$security_modules[] = $module_text;
}
}
// Send email notification.
if ($security_available == TRUE) {
// Send mail.
$site_mail = \Drupal::config('system.site')->get('mail_notification');
$params['message'] = t('New security updates for modules: @modules', array('@modules' => implode(', ', $security_modules)));
$result = \Drupal::service('plugin.manager.mail')->mail('autoupdate', 'new_security_updates', $email, 'en', $params, $site_mail);
if ($result['result'] == true) {
\Drupal::logger('autoupdate')->critical(t('New security updates for modules: @modules', array('@modules' => implode(', ', $security_modules))));
}
}
}
return $state_data;
}
/**
* Get version history on drupal.org.
*
* @param $module
* @param $module_version
*
* @return array
*/
private function getVersionHistory($module, $module_version) {
// Get module core version.
$core_module_version = $this->drupalCore . '.x';
$core_module_versions = explode('-', $module_version);
if (isset($core_module_versions[0])) {
$core_module_version = $core_module_versions[0];
}
$core_module_versions = explode('.', $module_version);
if (isset($core_module_versions[0])) {
$core_module_version = $core_module_versions[0] . '.x';
}
// Get data from drupal.org.
$url = 'https://updates.drupal.org/release-history/' . $module . '/' . $core_module_version . '?site_key=' . $this->siteKey;
if ($module == 'drupal') {
$url = 'https://updates.drupal.org/release-history/' . $module . '/current?site_key=' . $this->siteKey;
}
$history = simplexml_load_string(file_get_contents($url));
if (!isset($history->releases)) {
// Try with core version and last core version because some community modules dont have core affiliation.
$url = 'https://updates.drupal.org/release-history/' . $module . '/' . $this->drupalCore . '.x' . '?site_key=' . $this->siteKey;
$history = simplexml_load_string(file_get_contents($url));
if (!isset($history->releases)) {
$lastCore = (int) $this->drupalCore - 1;
$url = 'https://updates.drupal.org/release-history/' . $module . '/' . $lastCore . '.x' . '?site_key=' . $this->siteKey;
$history = simplexml_load_string(file_get_contents($url));
if (!isset($history->releases)) {
// Error no releases.
\Drupal::logger('autoupdate')->error('Error, no version history found for module @modulename (@url)', ['@modulename' => $module, '@url' => $url]);
return ['security' => NULL, 'insecure' => NULL, 'releases' => []];
}
}
}
// Extract local module version.
$module_versions = explode('.', $module_version);
$versions = '';
if (isset($module_versions[2])) {
$versions = $module_versions[2];
}
$module_versions_patch = explode('-', $versions);
$module_major = $module_versions[0];
$module_minor = $module_versions[1];
$module_patch = $module_versions_patch[0];
$module_extra = (isset($module_versions_patch[1]) ? $module_versions_patch[1] : NULL);
// Check history data.
$security = 0;
$insecure = 0;
$releases = array();
foreach ($history->releases->release as $release) {
// Skipp release if not compatible with our core version.
if (isset($release->core_compatibility)) {
if (strpos($release->core_compatibility->__toString(), '^' . $this->drupalCore) === FALSE) {
continue;
}
}
// Extract release module version.
$release_version = str_replace($this->drupalCore . '.x-', $this->drupalCore . '.', $release->version);
$release_versions = explode('.', $release_version);
$release_versions_patch = explode('-', $release_versions[2]);
$release_major = $release_versions[0];
$release_minor = $release_versions[1];
$release_patch = $release_versions_patch[0];
$release_extra = (isset($release_versions_patch[1]) ? $release_versions_patch[1] : NULL);
// Compare versions.
if ($module_major != $release_major) {
continue;
}
if ($module_minor != $release_minor) {
continue;
}
// Only until installed version.
if ($module_patch == $release_patch) {
break;
}
// Terms.
$terms = array();
if (isset($release->terms->term)) {
foreach ($release->terms->term as $term) {
$terms[] = $term->value->__toString();
}
}
if (isset($release->terms->value)) {
$terms[] = $release->terms->value->__toString();
}
foreach ($terms as $term) {
if ($term == 'Security update') {
$security = 1;
}
if ($term == 'Insecure') {
$insecure = 1;
}
}
$version = $release_major . '.' . $release_minor . '.' . $release_patch;
if ($release_extra != NULL) {
$version = $release_major . '.' . $release_minor . '.' . $release_patch . '-' . $release_extra;
}
$releases[] = array(
'version' => $version,
'orig_version' => str_replace($this->drupalCore . '.x-', '', $release->version),
'terms' => $terms,
);
}
krsort($releases);
return array('security' => $security, 'insecure' => $insecure, 'releases' => $releases);
}
/**
* Get data from composer file.
*
* @param $module
*
* @return mixed|null
*/
private function getComposerData($module) {
$composer = array();
$composer['composer'] = FALSE;
$composer['composer_path'] = NULL;
$composer['time'] = NULL;
$composer['version'] = NULL;
// If not exists return empty data.
$located = FALSE;
if (file_exists('composer.json') OR file_exists('composer.lock')) {
$located = array(
'composer.json' => 'composer.json',
'composer.lock' => 'composer.lock',
);
}
if ((file_exists('../composer.json') OR file_exists('../composer.lock')) AND $located == FALSE) {
$located = array(
'composer.json' => '../composer.json',
'composer.lock' => '../composer.lock',
);
}
if ($located == FALSE) {
return $composer;
}
// Get data form composer.json
if ($this->composerJson == NULL) {
$this->composerJson = file_get_contents($located['composer.json']);
}
$composer_json = json_decode($this->composerJson);
// Get data form composer.lock
if ($this->composerLock == NULL) {
$this->composerLock = file_get_contents($located['composer.lock']);
}
$composer_lock = json_decode($this->composerLock);
foreach ($composer_lock->packages as $package) {
if (strpos($package->name, '/'. $module) !== false) {
// Set composer data.
$composer['composer'] = TRUE;
$composer['composer_path'] = $package->name;
// Get time.
$time = NULL;
if (isset($package->time)) {
$time = strtotime($package->time);
}
$composer['time'] = $time;
// Get version form version tag.
$version = NULL;
if (isset($package->version)) {
// Only add version preffix on drupal.org modules.
if (strpos($composer['composer_path'], 'drupal/') !== false) {
$version = str_replace('v', $this->drupalCore . '.', $package->version);
}
else {
$version = str_replace('v', '', $package->version);
}
}
// Get version from extra.
if (isset($package->extra->drupal->version)) {
$version = str_replace($this->drupalCore . '.x-', $this->drupalCore . '.', $package->extra->drupal->version);
if (isset($package->extra->drupal->datestamp)) {
$composer['time'] = $package->extra->drupal->datestamp;
}
}
// Get version from source reference.
if (isset($package->source->reference) AND strlen($package->source->reference) < 20) {
$version = str_replace($this->drupalCore . '.x-', $this->drupalCore . '.', $package->source->reference);
}
$composer['version'] = $version;
// Get composer json setting.
$composer['composer_setting'] = NULL;
if (isset($composer_json->require)) {
foreach ($composer_json->require as $require_key => $require) {
if ($require_key == $composer['composer_path']) {
$composer['composer_setting'] = $require;
}
}
}
return $composer;
}
}
return $composer;
}
/**
* Get Drupal data (system_rebuild_module_data()).
*
* @return mixed
*/
private function getModuleData() {
$modules_cache =& drupal_static(__FUNCTION__);
// Only rebuild once per request. $modules and $modules_cache cannot be
// combined into one variable, because the $modules_cache variable is reset by
// reference from system_list_reset() during the rebuild.
if (!isset($modules_cache)) {
$modules = \Drupal::service('extension.list.module')->reset()->getList();
$files = array();
ksort($modules);
// Add status, weight, and schema version.
$installed_modules = \Drupal::config('core.extension')
->get('module') ?: array();
foreach ($modules as $name => $module) {
$module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0;
$module->status = (int) isset($installed_modules[$name]);
$module->schema_version = SCHEMA_UNINSTALLED;
$files[$name] = $module
->getPathname();
}
$modules = \Drupal::moduleHandler()
->buildModuleDependencies($modules);
$modules_cache = $modules;
// Store filenames to allow drupal_get_filename() to retrieve them without
// having to rebuild or scan the filesystem.
\Drupal::state()
->set('system.module.files', $files);
// Clear the module info cache.
\Drupal::cache()
->delete('system.module.info');
drupal_static_reset('system_get_info');
}
return $modules_cache;
}
}
