navigation_extra-1.0.x-dev/src/Plugin/Navigation/Extra/VersionPlugin.php
src/Plugin/Navigation/Extra/VersionPlugin.php
<?php
namespace Drupal\navigation_extra\Plugin\Navigation\Extra;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\InfoParserInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Url;
use Drupal\navigation_extra\NavigationExtraPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* An NavigationExtraPlugin for media navigation links.
*
* @NavigationExtraPlugin(
* id = "version",
* name = @Translation("Version"),
* description = @Translation("Displays a release version number and
* environment indicator."), weight = 0
* )
*/
class VersionPlugin extends NavigationExtraPluginBase {
/**
* The UUID service.
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
protected UuidInterface $uuidService;
/**
* The ModuleExtensionList service.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* The InfoParser service.
*
* @var \Drupal\Core\Extension\InfoParserInterface
*/
protected InfoParserInterface $infoParser;
/**
* The menu link manager service.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected MenuLinkManagerInterface $menuLinkManager;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected FileSystemInterface $fileSystem;
/**
* The current installed profile.
*
* @var string
*/
protected string $installProfile;
/**
* The request stack used to get the current request.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
private RequestStack $requestStack;
/**
* Constructs a \Drupal\Component\Plugin\PluginBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Extension\InfoParserInterface $info_parser
* The InfoParser service.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
* The ModuleExtensionList service.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param string $install_profile
* The name of the install profile.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack used to determine the current time.
*/
public function __construct(
array $configuration,
string $plugin_id,
mixed $plugin_definition,
LanguageManagerInterface $languageManager,
AccountProxyInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
RouteProviderInterface $route_provider,
ModuleHandlerInterface $module_handler,
ConfigFactoryInterface $config_factory,
EntityRepositoryInterface $entity_repository,
UuidInterface $uuid_service,
InfoParserInterface $info_parser,
ModuleExtensionList $module_extension_list,
MenuLinkManagerInterface $menu_link_manager,
FileSystemInterface $file_system,
string $install_profile,
RequestStack $request_stack,
) {
parent::__construct(
$configuration,
$plugin_id,
$plugin_definition,
$languageManager,
$current_user,
$entity_type_manager,
$route_provider,
$module_handler,
$config_factory,
$entity_repository
);
$this->uuidService = $uuid_service;
$this->infoParser = $info_parser;
$this->moduleExtensionList = $module_extension_list;
$this->menuLinkManager = $menu_link_manager;
$this->fileSystem = $file_system;
$this->installProfile = $install_profile;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager'),
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('router.route_provider'),
$container->get('module_handler'),
$container->get('config.factory'),
$container->get('entity.repository'),
$container->get('uuid'),
$container->get('info_parser'),
$container->get('extension.list.module'),
$container->get('plugin.manager.menu.link'),
$container->get('file_system'),
$container->getParameter('install_profile'),
$container->get('request_stack')
);
}
/**
* {@inheritdoc}
*/
public function buildConfigForm(array &$form, FormStateInterface $form_state): array {
$elements = parent::buildConfigForm($form, $form_state);
// Weight doesn't matter for version settings.
$elements['weight']['#type'] = 'hidden';
$elements['weight']['#value'] = 0;
$elements['info'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => [],
'#value' => $this->t('Important! Once enabled, goto %url and add a Navigation Extra - Version block to the navigation area.', [
'%url' => Link::fromTextAndUrl($this->t('Navigation blocks'), Url::fromUserInput('/admin/config/user-interface/navigation-block'))
->toString(),
]),
];
$elements += $this->buildConfigFormSourceFields($form, $form_state);
$elements += $this->buildConfigFormOutputFields($form, $form_state);
$triggering_element = $form_state->getTriggeringElement();
$elements['environments'] = [
'#type' => 'details',
'#open' => (bool) $triggering_element,
'#title' => $this->t('Environments'),
'#prefix' => '<div id="navigation-extra-version-environments-wrapper">',
'#suffix' => '</div>',
];
if ($triggering_element) {
$parents = $form_state->getTriggeringElement()['#parents'];
$action = array_pop($parents);
if ($action == 'delete') {
array_pop($parents);
}
$environments = $form_state->getValue($parents);
}
else {
$environments = $this->config->get('plugins.version.environments');
}
foreach ($environments ?? [] as $id => $environment) {
$elements['environments'][$id] = $this->buildConfigFormEnvironmentFields($form, $form_state, $environment, $id);
}
$elements['environments']['add'] = [
'#type' => 'submit',
'#value' => (string) $this->t('Add environment'),
'#submit' => [[$this, 'submitAddEnvironment']],
'#ajax' => [
'callback' => [$this, 'ajaxUpdateEnvironments'],
'wrapper' => 'navigation-extra-version-environments-wrapper',
],
'#name' => 'navigation-extra-version-environments-add',
];
return $elements;
}
/**
* Build Config Form Source Fields function.
*/
protected function buildConfigFormSourceFields(array &$form, FormStateInterface $form_state): array {
$elements = [];
$elements['source'] = [
'#type' => 'details',
'#title' => $this->t('Source'),
];
$elements['source']['provider'] = [
'#type' => 'select',
'#options' => [
'' => $this->t('None'),
'git_tag' => $this->t('Git tag'),
'module' => $this->t('Module info'),
'file' => $this->t('File'),
'env' => $this->t('Environment variable'),
],
'#title' => $this->t('Provider'),
'#description' => $this->t('The source provider to grab the version information from.'),
'#default_value' => $this->config->get("plugins.version.source.provider") ?? '',
];
/** @var \Drupal\Core\Extension\ExtensionList $list */
$list = $this->moduleExtensionList->getList();
$list_options = [];
foreach ($list as $name => $item) {
$list_options[$name] = $item->getName();
}
$elements['source']['module'] = [
'#type' => 'select',
'#options' => $list_options,
'#title' => $this->t('Module'),
'#description' => $this->t('Uses the module info.yml version setting.'),
'#default_value' => $this->config->get("plugins.version.source.module") ?? $this->installProfile,
'#states' => [
'visible' => [
[
':input[name="version[source][provider]"]' => ['value' => 'module'],
],
],
],
];
$elements['source']['file'] = [
'#type' => 'textfield',
'#title' => $this->t('File'),
'#description' => $this->t('The file to grab the version information from. Use a path relative to the webroot.'),
'#default_value' => $this->config->get("plugins.version.source.file") ?? '',
'#states' => [
'visible' => [
[
':input[name="version[source][provider]"]' => ['value' => 'file'],
],
],
],
];
$elements['source']['env'] = [
'#type' => 'textfield',
'#title' => $this->t('Environment variable'),
'#description' => $this->t('The environment variable to grab the version information from.'),
'#default_value' => $this->config->get("plugins.version.source.env") ?? '',
'#states' => [
'visible' => [
[
':input[name="version[source][provider]"]' => ['value' => 'env'],
],
],
],
];
$elements['source']['pattern'] = [
'#type' => 'textfield',
'#title' => $this->t('Pattern'),
'#description' => $this->t('A regex pattern to grab the version information from the source. See <a href="https://www.php.net/manual/en/function.preg-replace.php">preg_replace</a> for details.'),
'#placeholder' => '/^(\d+\.\d+\.\d+)$/',
'#default_value' => $this->config->get("plugins.version.source.pattern") ?? '',
'#states' => [
'invisible' => [
[
':input[name="version[source][provider]"]' => [
['value' => 'module'],
'or',
['value' => ''],
],
],
],
],
];
$elements['source']['format'] = [
'#type' => 'textfield',
'#title' => $this->t('Format'),
'#description' => $this->t('The source format string. Use capture groups from the source pattern. See <a href="https://www.php.net/manual/en/function.preg-replace.php">preg_replace</a> for details.'),
'#placeholder' => '$1',
'#default_value' => $this->config->get("plugins.version.source.format") ?? $this->config->get("plugins.version.source.format"),
'#states' => [
'invisible' => [
[
':input[name="version[source][provider]"]' => [
['value' => 'module'],
'or',
['value' => ''],
],
],
],
],
];
return $elements;
}
/**
* Build config form output fields function.
*/
protected function buildConfigFormOutputFields(array &$form, FormStateInterface $form_state): array {
$elements['output'] = [
'#type' => 'details',
'#title' => $this->t('Output'),
];
$elements['output']['title'] = [
'#type' => 'textfield',
'#title' => $this->t('Title format'),
'#description' => $this->t('The title format string. You can use %version, %drupal and %env as available tokens.'),
'#placeholder' => '%drupal - %version',
'#default_value' => $this->config->get("plugins.version.output.title") ?? '',
];
$elements['output']['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Description/tooltip format'),
'#description' => $this->t('The description format string. You can use %version, %drupal and %env as available tokens.'),
'#placeholder' => '%drupal - %version',
'#default_value' => $this->config->get("plugins.version.output.description") ?? '',
];
$elements['output']['url'] = [
'#type' => 'textfield',
'#title' => $this->t('URL'),
'#description' => $this->t('The output format url. Use %version as a token. You can also use a valid route name.'),
'#placeholder' => 'system.status',
'#default_value' => $this->config->get("plugins.version.output.url") ?? $this->config->get("plugins.version.output.url"),
];
$elements['output']['updates'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show updates'),
'#description' => $this->t('Notify user when there are updates available.'),
'#default_value' => $this->config->get("plugins.version.output.updates") ?? '',
];
return $elements;
}
/**
* Build config form environment fields function.
*/
protected function buildConfigFormEnvironmentFields(array &$form, FormStateInterface $form_state, $environment = [], $id = 0): array {
$default = $this->getDefaultEnvironment();
return [
'#type' => 'details',
'#title' => !empty($environment['name']) ? $environment['name'] : $this->t('Environment %id', ['%id' => $id]),
'#open' => FALSE,
'name' => [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#description' => $this->t('The name that should be displayed in the toolbar using the %env token.'),
'#default_value' => $environment['name'] ?? $default['name'],
],
'color' => [
'#type' => 'color',
'#title' => $this->t('Color'),
'#description' => $this->t('Pick a text color for the toolbar logo and version item'),
'#default_value' => $environment['color'] ?? $default['color'],
],
'background' => [
'#type' => 'color',
'#title' => $this->t('Background'),
'#description' => $this->t('Pick a background color for the toolbar logo and version item'),
'#default_value' => $environment['background'] ?? $default['background'],
],
'source' => [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Source'),
'provider' => [
'#type' => 'select',
'#title' => $this->t('Provider'),
'#options' => [
'domain' => $this->t('Domain'),
'env' => $this->t('Environment variable'),
'header' => $this->t('Header'),
],
'#description' => $this->t('Where to get the environment value from.'),
'#default_value' => $environment['source']['provider'] ?? $default['source']['provider'],
],
'env' => [
'#type' => 'textfield',
'#title' => $this->t('Environment variable'),
'#description' => $this->t('Enter the name of the $_ENV key containing the environment value'),
'#placeholder' => 'APPLICATION_ENV',
'#default_value' => $environment['source']['env'] ?? '',
'#states' => [
'visible' => [
[
':input[name="version[environments][' . $id . '][source][provider]"]' => ['value' => 'env'],
],
],
],
],
'header' => [
'#type' => 'textfield',
'#title' => $this->t('Header'),
'#description' => $this->t('Enter the name of the header containing the environment value'),
'#placeholder' => 'APPLICATION_ENV',
'#default_value' => $environment['source']['header'] ?? '',
'#states' => [
'visible' => [
[
':input[name="version[environments][' . $id . '][source][provider]"]' => ['value' => 'header'],
],
],
],
],
'pattern' => [
'#type' => 'textfield',
'#title' => $this->t('Pattern'),
'#description' => $this->t('A regex pattern to match the value of the source. See <a href="https://www.php.net/manual/en/function.preg-match.php">preg_match</a> for details.'),
'#default_value' => $environment['source']['pattern'] ?? $default['source']['pattern'],
],
],
"delete" => [
'#type' => 'submit',
'#value' => (string) $this->t('Delete'),
'#submit' => [[$this, 'submitDeleteEnvironment']],
'#ajax' => [
'callback' => [$this, 'ajaxUpdateEnvironments'],
'wrapper' => 'navigation-extra-version-environments-wrapper',
],
'#name' => 'navigation-extra-version-environments-delete-' . $id,
],
];
}
/**
* Submit add environment function.
*/
public static function submitAddEnvironment(array &$form, FormStateInterface $form_state): void {
$triggering_element = $form_state->getTriggeringElement();
$parents = $triggering_element['#parents'];
array_pop($parents);
$environments = $form_state->getValue($parents);
unset($environments['add']);
$environments[] = [];
$form_state->setValue($parents, $environments);
$form_state->setRebuild();
}
/**
* Submit delete environment function.
*/
public static function submitDeleteEnvironment(array &$form, FormStateInterface $form_state): void {
$triggering_element = $form_state->getTriggeringElement();
$parents = $triggering_element['#parents'];
array_pop($parents);
$id = array_pop($parents);
$environments = $form_state->getValue($parents);
unset($environments[$id]);
$form_state->setValue($parents, $environments);
$form_state->setRebuild();
}
/**
* AJAX update environments function.
*/
public static function ajaxUpdateEnvironments(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$array_parents = $triggering_element['#array_parents'];
$action = array_pop($array_parents);
if ($action === 'delete') {
array_pop($array_parents);
}
return NestedArray::getValue($form, $array_parents);
}
/**
* Get the version string.
*
* @return string
* Return the version.
*/
public function getVersion(): string {
return match ($this->config->get("plugins.version.source.provider") ?? '') {
'git_tag' => $this->getVersionFromGitTag(),
'module' => $this->getVersionFromModule(),
'env' => $this->getVersionFromEnvironmentVariable(),
'file' => $this->getVersionFromFile(),
default => '',
};
}
/**
* Get the formatted version string.
*
* @return string
* Return the formatted version.
*/
public function getVersionFormatted($format = 'title'): string {
$version = $this->getVersion();
$environment = $this->getEnvironment();
$env = $environment['name'] ?? '';
$drupal = \Drupal::VERSION;
$format = !empty($this->config->get("plugins.version.output.$format"))
? $this->config->get("plugins.version.output.$format")
: '%drupal - %version - %env';
$count = 0;
$version = str_replace(['%version', '%drupal', '%env'], [
$version,
$drupal,
$env,
], $format, $count);
$version = str_replace('- -', '-', $version);
$version = trim($version, " -");
return $count ? $version : '';
}
/**
* Get the url for linking to a release note page.
*
* @return string
* Return the version url.
*/
public function getVersionUrl(): string {
$version = $this->getVersion();
$url = !empty($this->config->get("plugins.version.output.url"))
? $this->config->get("plugins.version.output.url")
: 'system.status';
return str_replace('%version', $version, $url);
}
/**
* Get the module version from configured module or install profile.
*
* @return string
* Return the version string from the configured source module or profile.
*/
public function getVersionFromModule(): string {
$module = $this->config->get("plugins.version.source.module") ?? $this->installProfile;
$info = $this->moduleExtensionList->getExtensionInfo($module);
return $this->getVersionFromString($info['version'] ?? '');
}
/**
* Get the version from a git tag.
*
* @return string
* Return the version string from the configured git tag.
*/
public function getVersionFromGitTag(): string {
$files = glob($this->fileSystem->realpath(DRUPAL_ROOT . '/../.git/refs/tags/*'));
$tag = basename(end($files));
return $this->getVersionFromString($tag);
}
/**
* Get the version from a line in a file.
*
* @return string
* Return the version string from the configured git tag.
*/
public function getVersionFromFile(): string {
$version = '';
$file = $this->config->get("plugins.version.source.file") ?? '/';
$file_path = $this->fileSystem->realpath(DRUPAL_ROOT . $file);
if ($file_path) {
$pattern = !empty($this->config->get("plugins.version.source.pattern"))
? $this->config->get("plugins.version.source.pattern")
: '/^(\d+\.\d+\.\d+)$/';
$fh = fopen($file_path, 'r');
while (!feof($fh)) {
$line = fgets($fh, 4096);
if (preg_match($pattern, $line)) {
$format = !empty($this->config->get("plugins.version.source.format"))
? $this->config->get("plugins.version.source.format")
: '$1';
$version = preg_replace($pattern, $format, $line) ?? '';
break;
}
}
fclose($fh);
}
return $version;
}
/**
* Get the version from configured environment variable.
*
* @return string
* Return the version string from the configured environment variable.
*/
public function getVersionFromEnvironmentVariable(): string {
$variable = $this->config->get("plugins.version.source.env") ?? 'version';
$value = getenv($variable);
return $this->getVersionFromString($value);
}
/**
* Helper to grab a version from a string value.
*
* @param string|null $string
* The source string to grab the version from.
*
* @return string
* Returns the version as found by the configured source pattern and format.
*/
protected function getVersionFromString(?string $string): string {
$pattern = !empty($this->config->get("plugins.version.source.pattern"))
? $this->config->get("plugins.version.source.pattern")
: '/^[^0-9]*(\d+\.\d+\.\d+)[^0-9]*$/';
$format = !empty($this->config->get("plugins.version.source.format"))
? $this->config->get("plugins.version.source.format")
: '$1';
return (string) preg_replace($pattern, $format, $string ?: '') ?? '';
}
/**
* Get the environment config.
*
* @return array
* Return the environment config.
*/
public function getEnvironment(): array {
$match = [];
$environments = $this->config->get("plugins.version.environments") ?? [];
foreach ($environments as $environment) {
$match = match ($environment['source']['provider']) {
'header' => $this->getEnvironmentFromHeader($environment),
'env' => $this->getEnvironmentFromEnvironmentVariable($environment),
'domain' => $this->getEnvironmentFromDomain($environment),
default => [],
};
if ($match) {
break;
}
}
if (empty($match)) {
$match = $this->getDefaultEnvironment();
}
return $match;
}
/**
* Get the default environment settings.
*
* @return array
* An array with default values for an environment.
*/
protected function getDefaultEnvironment(): array {
return [
'background' => '#0000FF',
'color' => '#FFFFFF',
'name' => '',
'source' => [
'provider' => 'domain',
'pattern' => '/.*/',
],
];
}
/**
* Get environment from header function.
*/
public function getEnvironmentFromHeader($environment): array {
$header = $environment['source']['header'] ?? '';
$value = $_SERVER[$header] ?? '';
$pattern = !empty($environment['source']['pattern']) ? $environment['source']['pattern'] : '/no-valid-pattern/';
return preg_match($pattern, $value) ? $environment : [];
}
/**
* Get environment from environment variable function.
*/
public function getEnvironmentFromEnvironmentVariable($environment): array {
$variable = $environment['source']['env'] ?? '';
$value = getenv($variable) ?? '';
$pattern = !empty($environment['source']['pattern']) ? $environment['source']['pattern'] : '/no-valid-pattern/';
return preg_match($pattern, $value) ? $environment : [];
}
/**
* Get environment from domain function.
*/
public function getEnvironmentFromDomain($environment): array {
$value = $this->requestStack->getCurrentRequest()->getHost();
$pattern = !empty($environment['source']['pattern']) ? $environment['source']['pattern'] : '/no-valid-pattern/';
return preg_match($pattern, $value) ? $environment : [];
}
/**
* {@inheritdoc}
*/
public function alterDiscoveredMenuLinks(array &$links): void {
$linkSettings = [
'menu_name' => 'navigation_extra_version',
'title' => $this->getVersionFormatted(),
'description' => $this->getVersionFormatted('description') ?: $this->getVersionFormatted(),
'weight' => -100,
'options' => [
'icon' => [
'pack_id' => 'navigation_extra',
'icon_id' => 'version',
'settings' => [
'class' => 'toolbar-button__icon',
'size' => 25,
],
],
'attributes' => [
'class' => [
'navigation-extra--version',
],
],
],
];
$url = $this->getVersionUrl();
$fallback = '<front>';
if (empty($url)) {
// Everyone has access to <front>, system.status not.
$url = $fallback;
}
$isURL = FALSE;
$isRoute = FALSE;
try {
Url::fromUri($url);
$linkSettings['url'] = $url;
$isURL = TRUE;
}
catch (\Exception) {
}
if (!$isURL) {
try {
$this->routeProvider->getRouteByName($url);
$linkSettings['route_name'] = $url;
$isRoute = TRUE;
}
catch (RouteNotFoundException) {
}
}
if (!$isRoute && !$isURL) {
$linkSettings['route_name'] = $fallback;
}
// Create the root element if it does not exist.
// We will add it by default to the administration menu.
$this->addLink('navigation.version', $linkSettings, $links);
}
/**
* {@inheritdoc}
*/
public function pageAttachments(&$page): void {
$page['#attached']['library'][] = 'navigation_extra/navigation_extra_version';
$environment = $this->getEnvironment();
$page['#attached']['drupalSettings']['navigation_extra']['environment'] = $environment;
}
}
