updated core and modules

This commit is contained in:
Bachir Soussi Chiadmi
2017-09-09 16:27:51 +02:00
parent 027aa99b32
commit 41863f872c
7810 changed files with 318922 additions and 83631 deletions
@@ -0,0 +1,234 @@
<?php
namespace Drupal\devel\Commands;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Component\Uuid\Php;
use Drupal\Core\Utility\Token;
use Drush\Commands\DrushCommands;
use Drush\Exceptions\UserAbortException;
/**
* For commands that are parts of modules, Drush expects to find commandfiles in
* __MODULE__/src/Commands, and the namespace is Drupal/__MODULE__/Commands.
*
* In addition to a commandfile like this one, you need to add a drush.services.yml
* in root of your module like this module does.
*/
class DevelCommands extends DrushCommands {
protected $token;
protected $container;
protected $eventDispatcher;
public function __construct(Token $token, $container, $eventDispatcher) {
parent::__construct();
$this->token = $token;
$this->container = $container;
$this->eventDispatcher = $eventDispatcher;
}
/**
* @return mixed
*/
public function getEventDispatcher() {
return $this->eventDispatcher;
}
/**
* @return mixed
*/
public function getContainer() {
return $this->container;
}
/**
* @return Token
*/
public function getToken() {
return $this->token;
}
/**
* Uninstall, and Install a list of modules.
* @command devel-reinstall
* @param $modules A comma-separated list of module names.
* @aliases dre
* @allow-additional-options pm-uninstall,pm-enable
*/
public function reinstall($projects) {
$projects = _convert_csv_to_array($projects);
// This is faster than 3 separate bootstraps.
$args = array_merge(array('pm-uninstall'), $projects);
// @todo. Use $application dispatch instead of drush_invoke().
call_user_func_array('drush_invoke', $args);
$args = array_merge(array('pm-enable'), $projects);
call_user_func_array('drush_invoke', $args);
}
/**
* List implementations of a given hook and optionally edit one.
*
* @command devel-hook
* @param $hook The name of the hook to explore.
* @usage devel-hook cron
* List implementations of hook_cron().
* @aliases fnh,fn-hook,hook
*/
function hook($hook) {
// Get implementations in the .install files as well.
include_once './core/includes/install.inc';
drupal_load_updates();
if ($hook_implementations = \Drupal::moduleHandler()->getImplementations($hook)) {
if ($choice = drush_choice(array_combine($hook_implementations, $hook_implementations), 'Enter the number of the hook implementation you wish to view.')) {
$info= $this->codeLocate($choice . "_$hook");
$exec = drush_get_editor();
drush_shell_exec_interactive($exec, $info['file']);
}
}
else {
$this->logger()->success(dt('No implementations.'));
}
}
/**
* List implementations of a given event and optionally edit one.
*
* @command devel-event
* @param $event The name of the event to explore. If omitted, a list of events is shown.
* @usage devel-event
* Pick a Kernel event, then pick an implementation, and then view its source code.
* @usage devel-event kernel.terminate
* Pick a terminate subscribers and view its source code.
* @aliases fne,fn-event,event
*/
function event($event) {
$dispatcher = $this->getEventDispatcher();
if (empty($event)) {
// @todo Expand this list and move to interact().
$events = array('kernel.controller', 'kernel.exception', 'kernel.request', 'kernel.response', 'kernel.terminate', 'kernel.view');
$events = array_combine($events, $events);
if (!$event = drush_choice($events, 'Enter the event you wish to explore.')) {
throw new UserAbortException();
}
}
if ($implementations = $dispatcher->getListeners($event)) {
foreach ($implementations as $implementation) {
$callable = get_class($implementation[0]) . '::' . $implementation[1];
$choices[$callable] = $callable;
}
if ($choice = drush_choice($choices, 'Enter the number of the implementation you wish to view.')) {
$info= $this->codeLocate($choice);
$exec = drush_get_editor();
drush_shell_exec_interactive($exec, $info['file']);
}
}
else {
$this->logger()->success(dt('No implementations.'));
}
}
/**
* List available tokens.
*
* @command devel-token
* @aliases token
* @field-labels
* group: Group
* token: Token
* name: Name
* @default-fields group,token,name
*
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
*/
public function token($options = ['format' => 'table']) {
$all = $this->getToken()->getInfo();
foreach ($all['tokens'] as $group => $tokens) {
foreach ($tokens as $key => $token) {
$rows[] = [
'group' => $group,
'token' => $key,
'name' => $token['name'],
];
}
}
return new RowsOfFields($rows);
}
/**
* Generate a UUID.
*
* @command devel-uuid
* @aliases uuid
* @usage drush devel-uuid
* Outputs a Universally Unique Identifier.
*
* @return string
*/
public function uuid() {
$uuid = new Php();
return $uuid->generate();
}
/**
* Get source code line for specified function or method.
*/
function codeLocate($function_name) {
// Get implementations in the .install files as well.
include_once './core/includes/install.inc';
drupal_load_updates();
if (strpos($function_name, '::') === FALSE) {
if (!function_exists($function_name)) {
throw new \Exception(dt('Function not found'));
}
$reflect = new \ReflectionFunction($function_name);
}
else {
list($class, $method) = explode('::', $function_name);
if (!method_exists($class, $method)) {
throw new \Exception(dt('Method not found'));
}
$reflect = new \ReflectionMethod($class, $method);
}
return array('file' => $reflect->getFileName(), 'startline' => $reflect->getStartLine(), 'endline' => $reflect->getEndLine());
}
/**
* Get a list of available container services.
*
* @command devel-services
* @param $prefix A prefix to filter the service list by.
* @aliases devel-container-services,dcs
* @usage drush devel-services
* Gets a list of all available container services
* @usage drush dcs plugin.manager
* Get all services containing "plugin.manager"
*
* @return array
*/
public function services($prefix = NULL, $options = ['format' => 'yaml']) {
$container = $this->getContainer();
// Get a list of all available service IDs.
$services = $container->getServiceIds();
// If there is a prefix, try to find matches.
if (isset($prefix)) {
$services = preg_grep("/$prefix/", $services);
}
if (empty($services)) {
throw new \Exception(dt('No container services found.'));
}
sort($services);
return $services;
}
}
@@ -0,0 +1,274 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for the container info pages.
*/
class ContainerInfoController extends ControllerBase implements ContainerAwareInterface {
use ContainerAwareTrait;
/**
* The drupal kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $kernel;
/**
* The dumper manager service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* ServiceInfoController constructor.
*
* @param \Drupal\Core\DrupalKernelInterface $drupalKernel
* The drupal kernel.
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper manager service.
*/
public function __construct(DrupalKernelInterface $drupalKernel, DevelDumperManagerInterface $dumper) {
$this->kernel = $drupalKernel;
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('kernel'),
$container->get('devel.dumper')
);
}
/**
* Builds the services overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function serviceList() {
$headers = [
$this->t('ID'),
$this->t('Class'),
$this->t('Alias'),
$this->t('Operations'),
];
$rows = [];
if ($container = $this->kernel->getCachedContainerDefinition()) {
foreach ($container['services'] as $service_id => $definition) {
$service = unserialize($definition);
$row['id'] = [
'data' => $service_id,
'class' => 'table-filter-text-source',
];
$row['class'] = [
'data' => isset($service['class']) ? $service['class'] : '',
'class' => 'table-filter-text-source',
];
$row['alias'] = [
'data' => array_search($service_id, $container['aliases']) ?: '',
'class' => 'table-filter-text-source',
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.container_info.service.detail', ['service_id' => $service_id]),
],
],
];
$rows[$service_id] = $row;
}
ksort($rows);
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter service id, alias or class'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the service id, service alias or class to filter by.'),
],
];
$output['services'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No services found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-service-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the service.
*
* @param string $service_id
* The ID of the service to retrieve.
*
* @return array
* A render array containing the service detail.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested service is not defined.
*/
public function serviceDetail($service_id) {
$instance = $this->container->get($service_id, ContainerInterface::NULL_ON_INVALID_REFERENCE);
if ($instance === NULL) {
throw new NotFoundHttpException();
}
$output = [];
if ($cached_definitions = $this->kernel->getCachedContainerDefinition()) {
// Tries to retrieve the service definition from the kernel's cached
// container definition.
if (isset($cached_definitions['services'][$service_id])) {
$definition = unserialize($cached_definitions['services'][$service_id]);
// If the service has an alias add it to the definition.
if ($alias = array_search($service_id, $cached_definitions['aliases'])) {
$definition['alias'] = $alias;
}
$output['definition'] = $this->dumper->exportAsRenderable($definition, $this->t('Computed Definition'));
}
}
$output['instance'] = $this->dumper->exportAsRenderable($instance, $this->t('Instance'));
return $output;
}
/**
* Builds the parameters overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function parameterList() {
$headers = [
$this->t('Name'),
$this->t('Operations'),
];
$rows = [];
if ($container = $this->kernel->getCachedContainerDefinition()) {
foreach ($container['parameters'] as $parameter_name => $definition) {
$row['name'] = [
'data' => $parameter_name,
'class' => 'table-filter-text-source',
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.container_info.parameter.detail', ['parameter_name' => $parameter_name]),
],
],
];
$rows[$parameter_name] = $row;
}
ksort($rows);
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter parameter name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the parameter name to filter by.'),
],
];
$output['parameters'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No parameters found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-parameter-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the parameter value.
*
* @param string $parameter_name
* The name of the parameter to retrieve.
*
* @return array
* A render array containing the parameter value.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested parameter is not defined.
*/
public function parameterDetail($parameter_name) {
try {
$parameter = $this->container->getParameter($parameter_name);
}
catch (ParameterNotFoundException $e) {
throw new NotFoundHttpException();
}
return $this->dumper->exportAsRenderable($parameter);
}
}
@@ -3,19 +3,41 @@
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for devel module routes.
*/
class DevelController extends ControllerBase {
/**
* The dumper service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* EntityDebugController constructor.
*
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper service.
*/
public function __construct(DevelDumperManagerInterface $dumper) {
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('devel.dumper'));
}
/**
* Clears all caches, then redirects to the previous page.
*/
@@ -25,64 +47,10 @@ class DevelController extends ControllerBase {
return $this->redirect('<front>');
}
/**
* Returns a dump of a route object.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Page request object.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return array
* A render array containing the route object.
*/
public function menuItem(Request $request, RouteMatchInterface $route_match) {
$output = [];
// Get the route object from the path query string if available.
if ($path = $request->query->get('path')) {
try {
/* @var \Symfony\Cmf\Component\Routing\ChainRouter $router */
$router = \Drupal::service('router');
$route = $router->match($path);
$output['route'] = ['#markup' => kpr($route, TRUE)];
}
catch (\Exception $e) {
drupal_set_message($this->t("Unable to load route for url '%url'", ['%url' => $path]), 'warning');
}
}
// No path specified, get the current route.
else {
$route = $route_match->getRouteObject();
$output['route'] = ['#markup' => kpr($route, TRUE)];
}
return $output;
}
public function themeRegistry() {
$hooks = theme_get_registry();
ksort($hooks);
return array('#markup' => kprint_r($hooks, TRUE));
}
/**
* Builds the elements info overview page.
*
* @return array
* Array of page elements to render.
*/
public function elementsPage() {
$element_info_manager = \Drupal::service('element_info');
$elements_info = array();
foreach ($element_info_manager->getDefinitions() as $element_type => $definition) {
$elements_info[$element_type] = $definition + $element_info_manager->getInfo($element_type);
}
ksort($elements_info);
return array('#markup' => kdevel_print_object($elements_info));
return $this->dumper->exportAsRenderable($hooks);
}
/**
@@ -94,43 +62,31 @@ class DevelController extends ControllerBase {
public function fieldInfoPage() {
$fields = FieldStorageConfig::loadMultiple();
ksort($fields);
$output['fields'] = array('#markup' => kprint_r($fields, TRUE, $this->t('Fields')));
$output['fields'] = $this->dumper->exportAsRenderable($fields, $this->t('Fields'));
$field_instances = FieldConfig::loadMultiple();
ksort($field_instances);
$output['instances'] = array('#markup' => kprint_r($field_instances, TRUE, $this->t('Instances')));
$output['instances'] = $this->dumper->exportAsRenderable($field_instances, $this->t('Instances'));
$bundles = \Drupal::service('entity_type.bundle.info')->getAllBundleInfo();
ksort($bundles);
$output['bundles'] = array('#markup' => kprint_r($bundles, TRUE, $this->t('Bundles')));
$output['bundles'] = $this->dumper->exportAsRenderable($bundles, $this->t('Bundles'));
$field_types = \Drupal::service('plugin.manager.field.field_type')->getUiDefinitions();
ksort($field_types);
$output['field_types'] = array('#markup' => kprint_r($field_types, TRUE, $this->t('Field types')));
$output['field_types'] = $this->dumper->exportAsRenderable($field_types, $this->t('Field types'));
$formatter_types = \Drupal::service('plugin.manager.field.formatter')->getDefinitions();
ksort($formatter_types);
$output['formatter_types'] = array('#markup' => kprint_r($formatter_types, TRUE, $this->t('Formatter types')));
$output['formatter_types'] = $this->dumper->exportAsRenderable($formatter_types, $this->t('Formatter types'));
$widget_types = \Drupal::service('plugin.manager.field.widget')->getDefinitions();
ksort($widget_types);
$output['widget_types'] = array('#markup' => kprint_r($widget_types, TRUE, $this->t('Widget types')));
$output['widget_types'] = $this->dumper->exportAsRenderable($widget_types, $this->t('Widget types'));
return $output;
}
/**
* Builds the entity types overview page.
*
* @return array
* Array of page elements to render.
*/
public function entityInfoPage() {
$types = $this->entityTypeManager()->getDefinitions();
ksort($types);
return array('#markup' => kprint_r($types, TRUE));
}
/**
* Builds the state variable overview page.
*
@@ -181,7 +137,7 @@ class DevelController extends ControllerBase {
'class' => 'table-filter-text-source',
),
'value' => array(
'data' => kprint_r($state, TRUE),
'data' => $this->dumper->export($state),
),
);
@@ -225,9 +181,7 @@ class DevelController extends ControllerBase {
'#rows' => array(array(session_name(), session_id())),
'#empty' => $this->t('No session available.'),
);
$output['data'] = array(
'#markup' => kprint_r($_SESSION, TRUE),
);
$output['data'] = $this->dumper->exportAsRenderable($_SESSION);
return $output;
}
@@ -0,0 +1,162 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for the element info page.
*/
class ElementInfoController extends ControllerBase {
/**
* Element info manager service.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The dumper service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* EventInfoController constructor.
*
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* Element info manager service.
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper service.
*/
public function __construct(ElementInfoManagerInterface $element_info, DevelDumperManagerInterface $dumper) {
$this->elementInfo = $element_info;
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('element_info'),
$container->get('devel.dumper')
);
}
/**
* Builds the element overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function elementList() {
$headers = [
$this->t('Name'),
$this->t('Provider'),
$this->t('Class'),
$this->t('Operations'),
];
$rows = [];
foreach ($this->elementInfo->getDefinitions() as $element_type => $definition) {
$row['name'] = [
'data' => $element_type,
'class' => 'table-filter-text-source',
];
$row['provider'] = [
'data' => $definition['provider'],
'class' => 'table-filter-text-source',
];
$row['class'] = [
'data' => $definition['class'],
'class' => 'table-filter-text-source',
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.elements_page.detail', ['element_name' => $element_type]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[$element_type] = $row;
}
ksort($rows);
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter element id, provider or class'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the element id, provider or class to filter by.'),
],
];
$output['elements'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No elements found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-element-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the element.
*
* @param string $element_name
* The name of the element to retrieve.
*
* @return array
* A render array containing the element.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested element is not defined.
*/
public function elementDetail($element_name) {
if (!$element = $this->elementInfo->getDefinition($element_name, FALSE)) {
throw new NotFoundHttpException();
}
$element += $this->elementInfo->getInfo($element_name);
return $this->dumper->exportAsRenderable($element, $element_name);
}
}
@@ -41,6 +41,27 @@ class EntityDebugController extends ControllerBase {
return new static($container->get('devel.dumper'));
}
/**
* Returns the entity type definition of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityTypeDefinition(RouteMatchInterface $route_match) {
$output = [];
$entity = $this->getEntityFromRouteMatch($route_match);
if ($entity instanceof EntityInterface) {
$output = $this->dumper->exportAsRenderable($entity->getEntityType());
}
return $output;
}
/**
* Returns the loaded structure of the current entity.
*
@@ -0,0 +1,154 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for the entity types info page.
*/
class EntityTypeInfoController extends ControllerBase {
/**
* The dumper service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* EntityTypeInfoController constructor.
*
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper service.
*/
public function __construct(DevelDumperManagerInterface $dumper) {
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('devel.dumper')
);
}
/**
* Builds the entity types overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function entityTypeList() {
$headers = [
$this->t('ID'),
$this->t('Name'),
$this->t('Provider'),
$this->t('Class'),
$this->t('Operations'),
];
$rows = [];
foreach ($this->entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
$row['id'] = [
'data' => $entity_type->id(),
'class' => 'table-filter-text-source',
];
$row['name'] = [
'data' => $entity_type->getLabel(),
'class' => 'table-filter-text-source',
];
$row['provider'] = [
'data' => $entity_type->getProvider(),
'class' => 'table-filter-text-source',
];
$row['class'] = [
'data' => $entity_type->getClass(),
'class' => 'table-filter-text-source',
];
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.entity_info_page.detail', ['entity_type_id' => $entity_type_id]),
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 700,
'minHeight' => 500,
]),
],
],
],
];
$rows[$entity_type_id] = $row;
}
ksort($rows);
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter entity type id, provider or class'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the entity type id, provider or class to filter by.'),
],
];
$output['entities'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No entity types found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-entity-type-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the entity type.
*
* @param string $entity_type_id
* The name of the entity type to retrieve.
*
* @return array
* A render array containing the entity type.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* If the requested entity type is not defined.
*/
public function entityTypeDetail($entity_type_id) {
if (!$entity_type = $this->entityTypeManager()->getDefinition($entity_type_id, FALSE)) {
throw new NotFoundHttpException();
}
return $this->dumper->exportAsRenderable($entity_type, $entity_type_id);
}
}
@@ -0,0 +1,136 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides route responses for the event info page.
*/
class EventInfoController extends ControllerBase {
/**
* Event dispatcher service.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* EventInfoController constructor.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event dispatcher service.
*/
public function __construct(EventDispatcherInterface $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('event_dispatcher')
);
}
/**
* Builds the events overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function eventList() {
$headers = [
'name' => [
'data' => $this->t('Event Name'),
'class' => 'visually-hidden',
],
'callable' => $this->t('Callable'),
'priority' => $this->t('Priority'),
];
$event_listeners = $this->eventDispatcher->getListeners();
ksort($event_listeners);
$rows = [];
foreach ($event_listeners as $event_name => $listeners) {
$rows[][] = [
'data' => $event_name,
'class' => 'table-filter-text-source devel-event-name-header',
'colspan' => '3',
'header' => TRUE,
];
foreach ($listeners as $priority => $listener) {
$row['name'] = [
'data' => $event_name,
'class' => 'table-filter-text-source visually-hidden',
];
$row['class'] = [
'data' => $this->resolveCallableName($listener),
];
$row['priority'] = [
'data' => $priority,
];
$rows[] = $row;
}
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['name'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter event name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the event name to filter by.'),
],
];
$output['events'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No events found.'),
'#attributes' => [
'class' => ['devel-event-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Helper function for resolve callable name.
*
* @param mixed $callable
* The for which resolve the name. Can be either the name of a function
* stored in a string variable, or an object and the name of a method
* within the object.
*
* @return string
* The resolved callable name or an empty string.
*/
protected function resolveCallableName($callable) {
if (is_callable($callable, TRUE, $callable_name)) {
return $callable_name;
}
return '';
}
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns response for Layout Info route.
*/
class LayoutInfoController extends ControllerBase {
/**
* The Layout Plugin Manager.
*
* @var Drupal\Core\Layout\LayoutPluginManagerInterface
*/
protected $layoutPluginManager;
/**
* LayoutInfoController constructor.
*
* @param \Drupal\Core\Layout\LayoutPluginManagerInterface $pluginManagerLayout
* The layout manager.
*/
public function __construct(LayoutPluginManagerInterface $pluginManagerLayout) {
$this->layoutPluginManager = $pluginManagerLayout;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.core.layout')
);
}
/**
* Builds the Layout Info page.
*
* @return array
* Array of page elements to render.
*/
public function layoutInfoPage() {
$definedLayouts = [];
$layouts = $this->layoutPluginManager->getDefinitions();
foreach ($layouts as $layout) {
// @todo Revisit once https://www.drupal.org/node/2660124 gets in, getting
// the image should be as simple as $layout->getIcon().
$image = NULL;
if ($layout->getIconPath() != NULL) {
$image = [
'data' => [
'#theme' => 'image',
'#uri' => $layout->getIconPath(),
'#alt' => $layout->getLabel(),
'#height' => '65',
]
];
}
$definedLayouts[] = [
$image,
$layout->getLabel(),
$layout->getDescription(),
$layout->getCategory(),
implode(', ', $layout->getRegionLabels()),
$layout->getProvider(),
];
}
return [
'#theme' => 'table',
'#header' => [
$this->t('Icon'),
$this->t('Label'),
$this->t('Description'),
$this->t('Category'),
$this->t('Regions'),
$this->t('Provider'),
],
'#rows' => $definedLayouts,
'#empty' => $this->t('No layouts available.'),
'#attributes' => [
'class' => ['devel-layout-list'],
],
];
}
}
@@ -0,0 +1,203 @@
<?php
namespace Drupal\devel\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
/**
* Provides route responses for the route info pages.
*/
class RouteInfoController extends ControllerBase {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The router service.
*
* @var \Symfony\Component\Routing\RouterInterface
*/
protected $router;
/**
* The dumper service.
*
* @var \Drupal\devel\DevelDumperManagerInterface
*/
protected $dumper;
/**
* RouterInfoController constructor.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $provider
* The route provider.
* @param \Symfony\Component\Routing\RouterInterface $router
* The router service.
* @param \Drupal\devel\DevelDumperManagerInterface $dumper
* The dumper service.
*/
public function __construct(RouteProviderInterface $provider, RouterInterface $router, DevelDumperManagerInterface $dumper) {
$this->routeProvider = $provider;
$this->router = $router;
$this->dumper = $dumper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('router.route_provider'),
$container->get('router.no_access_checks'),
$container->get('devel.dumper')
);
}
/**
* Builds the routes overview page.
*
* @return array
* A render array as expected by the renderer.
*/
public function routeList() {
$headers = [
$this->t('Route Name'),
$this->t('Path'),
$this->t('Allowed Methods'),
$this->t('Operations'),
];
$rows = [];
foreach ($this->routeProvider->getAllRoutes() as $route_name => $route) {
$row['name'] = [
'data' => $route_name,
'class' => 'table-filter-text-source',
];
$row['path'] = [
'data' => $route->getPath(),
'class' => 'table-filter-text-source',
];
$row['methods']['data'] = [
'#theme' => 'item_list',
'#items' => $route->getMethods(),
'#empty' => $this->t('ANY'),
'#context' => ['list_style' => 'comma-list'],
];
// We cannot resolve routes with dynamic parameters from route path. For
// these routes we pass the route name.
// @see ::routeItem()
if (strpos($route->getPath(), '{') !== FALSE) {
$parameters = ['query' => ['route_name' => $route_name]];
}
else {
$parameters = ['query' => ['path' => $route->getPath()]];
}
$row['operations']['data'] = [
'#type' => 'operations',
'#links' => [
'devel' => [
'title' => $this->t('Devel'),
'url' => Url::fromRoute('devel.route_info.item', [], $parameters),
],
],
];
$rows[] = $row;
}
$output['#attached']['library'][] = 'system/drupal.system.modules';
$output['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$output['filters']['name'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter route name or path'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '.devel-filter-text',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the route name or path to filter by.'),
],
];
$output['routes'] = [
'#type' => 'table',
'#header' => $headers,
'#rows' => $rows,
'#empty' => $this->t('No routes found.'),
'#sticky' => TRUE,
'#attributes' => [
'class' => ['devel-route-list', 'devel-filter-text'],
],
];
return $output;
}
/**
* Returns a render array representation of the route object.
*
* The method tries to resolve the route from the 'path' or the 'route_name'
* query string value if available. If no route is retrieved from the query
* string parameters it fallbacks to the current route.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return array
* A render array as expected by the renderer.
*/
public function routeDetail(Request $request, RouteMatchInterface $route_match) {
$route = NULL;
// Get the route object from the path query string if available.
if ($path = $request->query->get('path')) {
try {
$route = $this->router->match($path);
}
catch (\Exception $e) {
drupal_set_message($this->t("Unable to load route for url '%url'", ['%url' => $path]), 'warning');
}
}
// Get the route object from the route name query string if available and
// the route is not retrieved by path.
if ($route === NULL && $route_name = $request->query->get('route_name')) {
try {
$route = $this->routeProvider->getRouteByName($route_name);
}
catch (\Exception $e) {
drupal_set_message($this->t("Unable to load route '%name'", ['%name' => $route_name]), 'warning');
}
}
// No route retrieved from path or name specified, get the current route.
if ($route === NULL) {
$route = $route_match->getRouteObject();
}
return $this->dumper->exportAsRenderable($route);
}
}
@@ -2,8 +2,8 @@
namespace Drupal\devel;
use Drupal\Core\Render\Markup;
use Drupal\Core\Plugin\PluginBase;
use Drupal\devel\Render\FilteredMarkup;
/**
* Defines a base devel dumper implementation.
@@ -15,6 +15,13 @@ use Drupal\Core\Plugin\PluginBase;
*/
abstract class DevelDumperBase extends PluginBase implements DevelDumperInterface {
/**
* {@inheritdoc}
*/
public function dump($input, $name = NULL) {
echo (string) $this->export($input, $name);
}
/**
* {@inheritdoc}
*/
@@ -32,7 +39,7 @@ abstract class DevelDumperBase extends PluginBase implements DevelDumperInterfac
* The unaltered input value.
*/
protected function setSafeMarkup($input) {
return Markup::create($input);
return FilteredMarkup::create($input);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Drupal\devel;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Manipulates entity type information.
*
* This class contains primarily bridged hooks for compile-time or
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
*/
class EntityTypeInfo implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* EntityTypeInfo constructor.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(AccountInterface $current_user) {
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('current_user')
);
}
/**
* Adds devel links to appropriate entity types.
*
* This is an alter hook bridge.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
* The master entity type list to alter.
*
* @see hook_entity_type_alter()
*/
public function entityTypeAlter(array &$entity_types) {
foreach ($entity_types as $entity_type_id => $entity_type) {
if (($entity_type->getFormClass('default') || $entity_type->getFormClass('edit')) && $entity_type->hasLinkTemplate('edit-form')) {
$entity_type->setLinkTemplate('devel-load', "/devel/$entity_type_id/{{$entity_type_id}}");
}
if ($entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical')) {
$entity_type->setLinkTemplate('devel-render', "/devel/$entity_type_id/{{$entity_type_id}}/render");
}
if ($entity_type->hasLinkTemplate('devel-render') || $entity_type->hasLinkTemplate('devel-load')) {
$entity_type->setLinkTemplate('devel-definition', "/devel/$entity_type_id/{{$entity_type_id}}/definition");
}
}
}
/**
* Adds devel operations on entity that supports it.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity on which to define an operation.
*
* @return array
* An array of operation definitions.
*
* @see hook_entity_operation()
*/
public function entityOperation(EntityInterface $entity) {
$operations = [];
if ($this->currentUser->hasPermission('access devel information')) {
if ($entity->hasLinkTemplate('devel-load')) {
$operations['devel'] = [
'title' => $this->t('Devel'),
'weight' => 100,
'url' => $entity->toUrl('devel-load'),
];
}
elseif ($entity->hasLinkTemplate('devel-render')) {
$operations['devel'] = [
'title' => $this->t('Devel'),
'weight' => 100,
'url' => $entity->toUrl('devel-render'),
];
}
}
return $operations;
}
}
@@ -1,108 +0,0 @@
<?php
namespace Drupal\devel\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class DevelEventSubscriber implements EventSubscriberInterface {
/**
* The devel.settings config object.
*
* @var \Drupal\Core\Config\Config;
*/
protected $config;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* Constructs a DevelEventSubscriber object.
*/
public function __construct(ConfigFactoryInterface $config, AccountInterface $account, ModuleHandlerInterface $module_handler, UrlGeneratorInterface $url_generator) {
$this->config = $config->get('devel.settings');
$this->account = $account;
$this->moduleHandler = $module_handler;
$this->urlGenerator = $url_generator;
}
/**
* Register the devel error handler.
*
* @param \Symfony\Component\EventDispatcher\Event $event
* The event to process.
*/
public function registerErrorHandler(Event $event = NULL) {
if ($this->account && $this->account->hasPermission('access devel information')) {
devel_set_handler(devel_get_handlers());
}
}
/**
* Initializes devel module requirements.
*/
public function onRequest(GetResponseEvent $event) {
if ($this->config->get('rebuild_theme')) {
drupal_theme_rebuild();
// Ensure that the active theme object is cleared.
$theme_name = \Drupal::theme()->getActiveTheme()->getName();
\Drupal::state()->delete('theme.active_theme.' . $theme_name);
\Drupal::theme()->resetActiveTheme();
/** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler*/
$theme_handler = \Drupal::service('theme_handler');
$theme_handler->refreshInfo();
// @todo This is not needed after https://www.drupal.org/node/2330755
$list = $theme_handler->listInfo();
$theme_handler->addTheme($list[$theme_name]);
if (\Drupal::service('flood')->isAllowed('devel.rebuild_theme_warning', 1)) {
\Drupal::service('flood')->register('devel.rebuild_theme_warning');
if ($this->account->hasPermission('access devel information')) {
drupal_set_message(t('The theme information is being rebuilt on every request. Remember to <a href=":url">turn off</a> this feature on production websites.', array(':url' => $this->urlGenerator->generateFromRoute('devel.admin_settings'))));
}
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Set a low value to start as early as possible.
$events[KernelEvents::REQUEST][] = ['onRequest', -100];
// Runs as soon as possible in the request but after
// AuthenticationSubscriber (priority 300) because you need to access to
// the current user for determine whether register the devel error handler
// or not.
$events[KernelEvents::REQUEST][] = ['registerErrorHandler', 256];
return $events;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Drupal\devel\EventSubscriber;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Listener for handling PHP errors.
*/
class ErrorHandlerSubscriber implements EventSubscriberInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $account;
/**
* ErrorHandlerSubscriber constructor.
*
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
*/
public function __construct(AccountProxyInterface $account) {
$this->account = $account;
}
/**
* Register devel error handler.
*
* @param \Symfony\Component\EventDispatcher\Event $event
* The event to process.
*/
public function registerErrorHandler(Event $event = NULL) {
if ($this->account && $this->account->hasPermission('access devel information')) {
devel_set_handler(devel_get_handlers());
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Runs as soon as possible in the request but after
// AuthenticationSubscriber (priority 300) because you need to access to
// the current user for determine whether register the devel error handler
// or not.
$events[KernelEvents::REQUEST][] = ['registerErrorHandler', 256];
return $events;
}
}
@@ -0,0 +1,116 @@
<?php
namespace Drupal\devel\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Subscriber for force the system to rebuild the theme registry.
*/
class ThemeInfoRebuildSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Internal flag for handle user notification.
*
* @var string
*/
protected $notificationFlag = 'devel.rebuild_theme_warning';
/**
* The devel config.
*
* @var \Drupal\Core\Config\Config;
*/
protected $config;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $account;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs a ThemeInfoRebuildSubscriber object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config factory.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ConfigFactoryInterface $config, AccountProxyInterface $account, ThemeHandlerInterface $theme_handler) {
$this->config = $config->get('devel.settings');
$this->account = $account;
$this->themeHandler = $theme_handler;
}
/**
* Forces the system to rebuild the theme registry.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The event to process.
*/
public function rebuildThemeInfo(GetResponseEvent $event) {
if ($this->config->get('rebuild_theme')) {
// Update the theme registry.
drupal_theme_rebuild();
// Refresh theme data.
$this->themeHandler->refreshInfo();
// Resets the internal state of the theme handler and clear the 'system
// list' cache; this allow to properly register, if needed, PSR-4
// namespaces for theme extensions after refreshing the info data.
$this->themeHandler->reset();
// Notify the user that the theme info are rebuilt on every request.
$this->triggerWarningIfNeeded($event->getRequest());
}
}
/**
* Notifies the user that the theme info are rebuilt on every request.
*
* The warning message is shown only to users with adequate permissions and
* only once per session.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*/
protected function triggerWarningIfNeeded(Request $request) {
if ($this->account && $this->account->hasPermission('access devel information')) {
$session = $request->getSession();
if (!$session->has($this->notificationFlag)) {
$session->set($this->notificationFlag, TRUE);
$message = $this->t('The theme information is being rebuilt on every request. Remember to <a href=":url">turn off</a> this feature on production websites.', [':url' => Url::fromRoute('devel.admin_settings')->toString()]);
drupal_set_message($message, 'warning', TRUE);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Set high priority value to start as early as possible.
$events[KernelEvents::REQUEST][] = ['rebuildThemeInfo', 256];
return $events;
}
}
@@ -32,7 +32,7 @@ class ConfigEditor extends FormBase {
return;
}
$data = $config->get();
$data = $config->getOriginal();
if (empty($data)) {
drupal_set_message(t('Config @name exists but has no data.', array('@name' => $config_name)), 'warning');
@@ -11,7 +11,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides confirmation form for rebuilding the routes.
*/
class DevelRebuildMenus extends ConfirmFormBase {
class RouterRebuildConfirmForm extends ConfirmFormBase {
/**
* The route builder service.
@@ -21,7 +21,7 @@ class DevelRebuildMenus extends ConfirmFormBase {
protected $routeBuilder;
/**
* Constructs a new DevelRebuildMenus object.
* Constructs a new RouterRebuildConfirmForm object.
*
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The route builder service.
@@ -50,7 +50,7 @@ class DevelRebuildMenus extends ConfirmFormBase {
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to rebuild menus?');
return $this->t('Are you sure you want to rebuild the router?');
}
/**
@@ -64,7 +64,7 @@ class DevelRebuildMenus extends ConfirmFormBase {
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Rebuild menu based on hook_menu() and revert any custom changes. All menu items return to their default settings.');
return $this->t('Rebuilds the routes information gathering all routing data from .routing.yml files and from classes which subscribe to the route build events. This action cannot be undone.');
}
/**
@@ -79,7 +79,7 @@ class DevelRebuildMenus extends ConfirmFormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->routeBuilder->rebuild();
drupal_set_message($this->t('The menu router has been rebuilt.'));
drupal_set_message($this->t('The router has been rebuilt.'));
$form_state->setRedirect('<front>');
}
@@ -73,6 +73,12 @@ class SettingsForm extends ConfigFormBase {
'#default_value' => $devel_config->get('raw_names'),
'#description' => t('Display the language-independent machine names of the permissions in mouse-over hints on the <a href=":permissions_url">Permissions</a> page and the module base file names on the Permissions and <a href=":modules_url">Modules</a> pages.', array(':permissions_url' => Url::fromRoute('user.admin_permissions')->toString(), ':modules_url' => Url::fromRoute('system.modules_list')->toString())),
);
$form['rebuild_theme'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Rebuild the theme registry on every page load'),
'#description' => $this->t('New templates, theme overrides, and changes to the theme.info.yml need the theme registry to be rebuilt in order to appear on the site.'),
'#default_value' => $devel_config->get('rebuild_theme'),
);
$error_handlers = devel_get_handlers();
$form['error_handlers'] = array(
@@ -116,13 +122,6 @@ class SettingsForm extends ConfigFormBase {
$request->query->remove('demo');
}
$form['rebuild_theme'] = array(
'#type' => 'checkbox',
'#title' => t('Rebuild the theme information like the registry'),
'#description' => t('While creating new templates, change the $theme.info.yml and theme_ overrides the theme information needs to be rebuilt.'),
'#default_value' => $devel_config->get('rebuild_theme'),
);
$dumper = $devel_config->get('devel_dumper');
$default = $this->dumperManager->isPluginSupported($dumper) ? $dumper : $this->dumperManager->getFallbackPluginId(NULL);
@@ -71,7 +71,7 @@ class SystemStateEdit extends FormBase {
$form['value'] = array(
'#type' => 'item',
'#title' => $this->t('Current value for %name', array('%name' => $state_name)),
'#markup' => kprint_r($old_value, TRUE),
'#markup' => kpr($old_value, TRUE),
);
$transport = 'plain';
@@ -0,0 +1,118 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configures devel toolbar settings.
*/
class ToolbarSettingsForm extends ConfigFormBase {
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTree
*/
protected $menuLinkTree;
/**
* ToolbarSettingsForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
*/
public function __construct(ConfigFactoryInterface $config_factory, MenuLinkTreeInterface $menu_link_tree) {
parent::__construct($config_factory);
$this->menuLinkTree = $menu_link_tree;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('menu.link_tree')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'devel_toolbar_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'devel.toolbar.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('devel.toolbar.settings');
$form['toolbar_items'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Menu items always visible'),
'#options' => $this->getLinkLabels(),
'#default_value' => $config->get('toolbar_items') ?: [],
'#required' => TRUE,
'#description' => $this->t('Select the menu items always visible in devel toolbar tray. All the items not selected in this list will be visible only when the toolbar orientation is vertical.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$toolbar_items = array_keys(array_filter($values['toolbar_items']));
$this->config('devel.toolbar.settings')
->set('toolbar_items', $toolbar_items)
->save();
parent::submitForm($form, $form_state);
}
/**
* Provides an array of available menu items.
*
* @return array
* Associative array of devel menu item labels keyed by plugin ID.
*/
protected function getLinkLabels() {
$options = [];
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = $this->menuLinkTree->load('devel', $parameters);
foreach ($tree as $element) {
$link = $element->link;
$options[$link->getPluginId()] = $link->getTitle();
}
asort($options);
return $options;
}
}
@@ -53,7 +53,7 @@ class DevelLocalTask extends DeriverBase implements ContainerDeriverInterface {
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = array();
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
@@ -62,29 +62,36 @@ class DevelLocalTask extends DeriverBase implements ContainerDeriverInterface {
if ($has_edit_path || $has_canonical_path) {
$this->derivatives["$entity_type_id.devel_tab"] = array(
$this->derivatives["$entity_type_id.devel_tab"] = [
'route_name' => "entity.$entity_type_id." . ($has_edit_path ? 'devel_load' : 'devel_render'),
'title' => $this->t('Devel'),
'base_route' => "entity.$entity_type_id." . ($has_canonical_path ? "canonical" : "edit_form"),
'weight' => 100,
);
];
$this->derivatives["$entity_type_id.devel_definition_tab"] = [
'route_name' => "entity.$entity_type_id.devel_definition",
'title' => $this->t('Definition'),
'parent_id' => "devel.entities:$entity_type_id.devel_tab",
'weight' => 100,
];
if ($has_canonical_path) {
$this->derivatives["$entity_type_id.devel_render_tab"] = array(
$this->derivatives["$entity_type_id.devel_render_tab"] = [
'route_name' => "entity.$entity_type_id.devel_render",
'weight' => 100,
'title' => $this->t('Render'),
'parent_id' => "devel.entities:$entity_type_id.devel_tab",
);
];
}
if ($has_edit_path) {
$this->derivatives["$entity_type_id.devel_load_tab"] = array(
$this->derivatives["$entity_type_id.devel_load_tab"] = [
'route_name' => "entity.$entity_type_id.devel_load",
'weight' => 100,
'title' => $this->t('Load'),
'parent_id' => "devel.entities:$entity_type_id.devel_tab",
);
];
}
}
}
@@ -3,6 +3,7 @@
namespace Drupal\devel\Plugin\Devel\Dumper;
use Doctrine\Common\Util\Debug;
use Drupal\Component\Utility\Xss;
use Drupal\devel\DevelDumperBase;
/**
@@ -16,16 +17,6 @@ use Drupal\devel\DevelDumperBase;
*/
class DoctrineDebug extends DevelDumperBase {
/**
* {@inheritdoc}
*/
public function dump($input, $name = NULL) {
if ($name) {
echo $name . ' => ';
}
Debug::dump($input);
}
/**
* {@inheritdoc}
*/
@@ -38,6 +29,10 @@ class DoctrineDebug extends DevelDumperBase {
$dump = ob_get_contents();
ob_end_clean();
// Run Xss::filterAdmin on the resulting string to prevent
// cross-site-scripting (XSS) vulnerabilities.
$dump = Xss::filterAdmin($dump);
$dump = '<pre>' . $name . $dump . '</pre>';
return $this->setSafeMarkup($dump);
@@ -3,6 +3,7 @@
namespace Drupal\devel\Plugin\Devel\Dumper;
use Drupal\Component\Utility\Variable;
use Drupal\Component\Utility\Xss;
use Drupal\devel\DevelDumperBase;
/**
@@ -16,21 +17,16 @@ use Drupal\devel\DevelDumperBase;
*/
class DrupalVariable extends DevelDumperBase {
/**
* {@inheritdoc}
*/
public function dump($input, $name = NULL) {
$name = $name ? $name . ' => ' : '';
$output = Variable::export($input);
echo '<pre>' . $name . print_r($output) . '</pre>';
}
/**
* {@inheritdoc}
*/
public function export($input, $name = NULL) {
$name = $name ? $name . ' => ' : '';
$dump = '<pre>' . $name . Variable::export($input) . '</pre>';
$dump = Variable::export($input);
// Run Xss::filterAdmin on the resulting string to prevent
// cross-site-scripting (XSS) vulnerabilities.
$dump = Xss::filterAdmin($dump);
$dump = '<pre>' . $name . $dump . '</pre>';
return $this->setSafeMarkup($dump);
}
@@ -19,13 +19,6 @@ use Symfony\Component\VarDumper\Dumper\HtmlDumper;
*/
class VarDumper extends DevelDumperBase {
/**
* {@inheritdoc}
*/
public function dump($input, $name = NULL) {
echo (string) $this->export($input, $name);
}
/**
* {@inheritdoc}
*/
@@ -37,6 +30,10 @@ class VarDumper extends DevelDumperBase {
$dumper->dump($cloner->cloneVar($input), $output);
$output = stream_get_contents($output, -1, 0);
if ($name) {
$output = $name . ' => ' . $output;
}
return $this->setSafeMarkup($output);
}
@@ -3,9 +3,12 @@
namespace Drupal\devel\Plugin\Mail;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Mail\Plugin\Mail\PhpMail;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Mail\MailInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Site\Settings;
use Exception;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a mail backend that saves emails as temporary files.
@@ -39,10 +42,86 @@ use Exception;
* description = @Translation("Outputs the message as a file in the temporary directory.")
* )
*/
class DevelMailLog extends PhpMail {
class DevelMailLog implements MailInterface, ContainerFactoryPluginInterface {
public function composeMessage($message) {
$mimeheaders = array();
/**
* The devel.settings config object.
*
* @var \Drupal\Core\Config\Config;
*/
protected $config;
/**
* Constructs a new DevelMailLog 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\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) {
$this->config = $config_factory->get('devel.settings');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function mail(array $message) {
$directory = $this->config->get('debug_mail_directory');
if (!$this->prepareDirectory($directory)) {
return FALSE;
}
$pattern = $this->config->get('debug_mail_file_format');
$filename = $this->replacePlaceholders($pattern, $message);
$output = $this->composeMessage($message);
return (bool) file_put_contents($directory . '/' . $filename, $output);
}
/**
* {@inheritdoc}
*/
public function format(array $message) {
// Join the body array into one string.
$message['body'] = implode("\n\n", $message['body']);
// Convert any HTML to plain-text.
$message['body'] = MailFormatHelper::htmlToText($message['body']);
// Wrap the mail body for sending.
$message['body'] = MailFormatHelper::wrapMail($message['body']);
return $message;
}
/**
* Compose the output message.
*
* @param array $message
* A message array, as described in hook_mail_alter().
*
* @return string
* The output message.
*/
protected function composeMessage($message) {
$mimeheaders = [];
$message['headers']['To'] = $message['to'];
foreach ($message['headers'] as $name => $value) {
$mimeheaders[] = $name . ': ' . Unicode::mimeHeaderEncode($value);
@@ -58,41 +137,52 @@ class DevelMailLog extends PhpMail {
return $output;
}
public function getFileName($message) {
$output_directory = $this->getOutputDirectory();
$this->makeOutputDirectory($output_directory);
$output_file_format = \Drupal::config('devel.settings')->get('debug_mail_file_format');
$tokens = array(
/**
* Replaces placeholders with sanitized values in a string.
*
* @param $filename
* The string that contains the placeholders. The following placeholders
* are considered in the replacement:
* - %to: replaced by the email recipient value.
* - %subject: replaced by the email subject value.
* - %datetime: replaced by the current datetime in 'y-m-d_his' format.
* @param array $message
* A message array, as described in hook_mail_alter().
*
* @return string
* The formatted string.
*/
protected function replacePlaceholders($filename, $message) {
$tokens = [
'%to' => $message['to'],
'%subject' => $message['subject'],
'%datetime' => date('y-m-d_his'),
);
return $output_directory . '/' . $this->dirify(str_replace(array_keys($tokens), array_values($tokens), $output_file_format));
}
private function dirify($string) {
return preg_replace('/[^a-zA-Z0-9_\-\.@]/', '_', $string);
];
$filename = str_replace(array_keys($tokens), array_values($tokens), $filename);
return preg_replace('/[^a-zA-Z0-9_\-\.@]/', '_', $filename);
}
/**
* {@inheritdoc}
* Checks that the directory exists and is writable.
* Public directories will be protected by adding an .htaccess which
* indicates that the directory is private.
*
* @param $directory
* A string reference containing the name of a directory path or URI.
*
* @return bool
* TRUE if the directory exists (or was created), is writable and is
* protected (if it is public). FALSE otherwise.
*/
public function mail(array $message) {
$output = $this->composeMessage($message);
$output_file = $this->getFileName($message);
return file_put_contents($output_file, $output);
}
protected function makeOutputDirectory($output_directory) {
if (!file_prepare_directory($output_directory, FILE_CREATE_DIRECTORY)) {
throw new Exception("Unable to continue sending mail, $output_directory is not writable");
protected function prepareDirectory($directory) {
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
return FALSE;
}
if (0 === strpos($directory, 'public://')) {
return file_save_htaccess($directory);
}
}
public function getOutputDirectory() {
return \Drupal::config('devel.settings')->get('debug_mail_directory');
return TRUE;
}
}
@@ -2,28 +2,10 @@
namespace Drupal\devel\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Url;
/**
* Modifies the menu link to add current route path.
*
* @deprecated in Devel 8.1.0-beta1, will be removed before Devel 8.1.0.
* Use \Drupal\devel\Plugin\Menu\RouteDetailMenuLink instead.
*/
class MenuItemMenuLink extends MenuLinkDefault {
/**
* {@inheritdoc}
*/
public function getOptions() {
$options = parent::getOptions();
$options['query']['path'] = '/' . Url::fromRoute('<current>')->getInternalPath();
return $options;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return 0;
}
}
class MenuItemMenuLink extends RouteDetailMenuLink {}
@@ -0,0 +1,29 @@
<?php
namespace Drupal\devel\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Url;
/**
* Modifies the menu link to add current route path.
*/
class RouteDetailMenuLink extends MenuLinkDefault {
/**
* {@inheritdoc}
*/
public function getOptions() {
$options = parent::getOptions();
$options['query']['path'] = '/' . Url::fromRoute('<current>')->getInternalPath();
return $options;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return 0;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\devel\Render;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\MarkupTrait;
/**
* Defines an object that passes safe strings through the Devel system.
*
* This object should only be constructed with a known safe string. If there is
* any risk that the string contains user-entered data that has not been
* filtered first, it must not be used.
*
* @internal
* This object is marked as internal because it should only be used in the
* Devel module.
* @see \Drupal\Core\Render\Markup
*/
final class FilteredMarkup implements MarkupInterface, \Countable {
use MarkupTrait;
}
@@ -45,11 +45,14 @@ class RouteSubscriber extends RouteSubscriberBase {
if ($route = $this->getEntityRenderRoute($entity_type)) {
$collection->add("entity.$entity_type_id.devel_render", $route);
}
if ($route = $this->getEntityTypeDefinitionRoute($entity_type)) {
$collection->add("entity.$entity_type_id.devel_definition", $route);
}
}
}
/**
* Gets the devel load route.
* Gets the entity load route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
@@ -80,7 +83,7 @@ class RouteSubscriber extends RouteSubscriberBase {
}
/**
* Gets the devel render route.
* Gets the entity render route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
@@ -110,6 +113,37 @@ class RouteSubscriber extends RouteSubscriberBase {
}
}
/**
* Gets the entity type definition route.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityTypeDefinitionRoute(EntityTypeInterface $entity_type) {
if ($devel_definition = $entity_type->getLinkTemplate('devel-definition')) {
$entity_type_id = $entity_type->id();
$route = new Route($devel_definition);
$route
->addDefaults([
'_controller' => '\Drupal\devel\Controller\EntityDebugController::entityTypeDefinition',
'_title' => 'Entity type definition',
])
->addRequirements([
'_permission' => 'access devel information'
])
->setOption('_admin_route', TRUE)
->setOption('_devel_entity_type_id', $entity_type_id)
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
return $route;
}
}
/**
* {@inheritdoc}
*/
@@ -65,11 +65,15 @@ class DevelControllerTest extends WebTestBase {
$this->assertText('Devel', 'Devel tab is present');
$this->drupalGet('devel/entity_test/' . $this->entity->id());
$this->assertResponse(200);
$this->assertText('Definition', 'Devel definition tab is present');
$this->assertText('Load', 'Devel load tab is present');
$this->assertText('Render', 'Devel load tab is present');
$this->assertLinkByHref('devel/entity_test/' . $this->entity->id() . '/render');
$this->drupalGet('devel/entity_test/' . $this->entity->id() . '/render');
$this->assertResponse(200);
$this->assertLinkByHref('devel/entity_test/' . $this->entity->id() . '/definition');
$this->drupalGet('devel/entity_test/' . $this->entity->id() . '/definition');
$this->assertResponse(200);
// Test Devel load and render routes for entities with only canonical route
// definitions.
@@ -82,6 +86,9 @@ class DevelControllerTest extends WebTestBase {
$this->assertResponse(404);
$this->drupalGet('devel/devel_entity_test_canonical/' . $this->entity_canonical->id() . '/render');
$this->assertResponse(200);
$this->assertLinkByHref('devel/devel_entity_test_canonical/' . $this->entity_canonical->id() . '/definition');
$this->drupalGet('devel/devel_entity_test_canonical/' . $this->entity_canonical->id() . '/definition');
$this->assertResponse(200);
// Test Devel load and render routes for entities with only edit route
// definitions.
@@ -89,10 +96,13 @@ class DevelControllerTest extends WebTestBase {
$this->assertText('Devel', 'Devel tab is present');
$this->assertLinkByHref('devel/devel_entity_test_edit/' . $this->entity_edit->id());
$this->assertNoLinkByHref('devel/devel_entity_test_edit/' . $this->entity_edit->id() . '/render');
$this->assertNoLinkByHref('devel/devel_entity_test_edit/' . $this->entity_edit->id() . '/definition');
$this->drupalGet('devel/devel_entity_test_edit/' . $this->entity_edit->id());
$this->assertResponse(200);
$this->drupalGet('devel/devel_entity_test_edit/' . $this->entity_edit->id() . '/render');
$this->assertResponse(404);
$this->drupalGet('devel/devel_entity_test_edit/' . $this->entity_edit->id() . '/definition');
$this->assertResponse(200);
// Test Devel load and render routes for entities with no route
// definitions.
@@ -100,10 +110,13 @@ class DevelControllerTest extends WebTestBase {
$this->assertNoText('Devel', 'Devel tab is not present');
$this->assertNoLinkByHref('devel/devel_entity_test_no_links/' . $this->entity_no_links->id());
$this->assertNoLinkByHref('devel/devel_entity_test_no_links/' . $this->entity_no_links->id() . '/render');
$this->assertNoLinkByHref('devel/devel_entity_test_no_links/' . $this->entity_no_links->id() . '/definition');
$this->drupalGet('devel/devel_entity_test_no_links/' . $this->entity_no_links->id());
$this->assertResponse(404);
$this->drupalGet('devel/devel_entity_test_no_links/' . $this->entity_no_links->id() . '/render');
$this->assertResponse(404);
$this->drupalGet('devel/devel_entity_test_no_links/' . $this->entity_no_links->id() . '/definition');
$this->assertResponse(404);
}
}
@@ -1,213 +0,0 @@
<?php
namespace Drupal\devel\Tests;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\simpletest\WebTestBase;
/**
* Tests devel error handler.
*
* @group devel
*/
class DevelErrorHandlerTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['devel'];
/**
* Tests devel error handler.
*/
public function testErrorHandler() {
$error_notice = [
'%type' => 'Notice',
'@message' => 'Undefined variable: undefined',
'%function' => 'Drupal\devel\Form\SettingsForm->demonstrateErrorHandlers()',
];
$error_warning = [
'%type' => 'Warning',
'@message' => 'Division by zero',
'%function' => 'Drupal\devel\Form\SettingsForm->demonstrateErrorHandlers()',
];
$config = $this->config('system.logging');
$config->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save();
$admin_user = $this->drupalCreateUser(['administer site configuration', 'access devel information']);
$this->drupalLogin($admin_user);
// Ensures that the error handler config is present on the config page and
// by default the standard error handler is selected.
$error_handlers = \Drupal::config('devel.settings')->get('error_handlers');
$this->assertEqual($error_handlers, [DEVEL_ERROR_HANDLER_STANDARD => DEVEL_ERROR_HANDLER_STANDARD]);
$this->drupalGet('admin/config/development/devel');
$this->assertOptionSelected('edit-error-handlers', DEVEL_ERROR_HANDLER_STANDARD);
// Ensures that selecting the DEVEL_ERROR_HANDLER_NONE option no error
// (raw or message) is shown on the site in case of php errors.
$edit = [
'error_handlers[]' => DEVEL_ERROR_HANDLER_NONE,
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$error_handlers = \Drupal::config('devel.settings')->get('error_handlers');
$this->assertEqual($error_handlers, [DEVEL_ERROR_HANDLER_NONE => DEVEL_ERROR_HANDLER_NONE]);
$this->assertOptionSelected('edit-error-handlers', DEVEL_ERROR_HANDLER_NONE);
$this->clickLink('notice+warning');
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->assertNoRawErrorMessage($error_notice);
$this->assertNoRawErrorMessage($error_warning);
$this->assertNoErrorMessage($error_notice);
$this->assertNoErrorMessage($error_warning);
// Ensures that selecting the DEVEL_ERROR_HANDLER_BACKTRACE_KINT option a
// backtrace above the rendered page is shown on the site in case of php
// errors.
$edit = [
'error_handlers[]' => DEVEL_ERROR_HANDLER_BACKTRACE_KINT,
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$error_handlers = \Drupal::config('devel.settings')->get('error_handlers');
$this->assertEqual($error_handlers, [DEVEL_ERROR_HANDLER_BACKTRACE_KINT => DEVEL_ERROR_HANDLER_BACKTRACE_KINT]);
$this->assertOptionSelected('edit-error-handlers', DEVEL_ERROR_HANDLER_BACKTRACE_KINT);
$this->clickLink('notice+warning');
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->assertRawErrorMessage($error_notice);
$this->assertRawErrorMessage($error_warning);
// Ensures that selecting the DEVEL_ERROR_HANDLER_BACKTRACE_DPM option a
// backtrace in the message area is shown on the site in case of php errors.
$edit = [
'error_handlers[]' => DEVEL_ERROR_HANDLER_BACKTRACE_DPM,
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$error_handlers = \Drupal::config('devel.settings')->get('error_handlers');
$this->assertEqual($error_handlers, [DEVEL_ERROR_HANDLER_BACKTRACE_DPM => DEVEL_ERROR_HANDLER_BACKTRACE_DPM]);
$this->assertOptionSelected('edit-error-handlers', DEVEL_ERROR_HANDLER_BACKTRACE_DPM);
$this->clickLink('notice+warning');
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->assertErrorMessage($error_notice);
$this->assertErrorMessage($error_warning);
// Ensures that when multiple handlers are selected, the output produced by
// every handler is shown on the site in case of php errors.
$edit = [
'error_handlers[]' => [
DEVEL_ERROR_HANDLER_BACKTRACE_DPM => DEVEL_ERROR_HANDLER_BACKTRACE_DPM,
DEVEL_ERROR_HANDLER_BACKTRACE_KINT => DEVEL_ERROR_HANDLER_BACKTRACE_KINT,
]
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$error_handlers = \Drupal::config('devel.settings')->get('error_handlers');
$this->assertEqual($error_handlers, [
DEVEL_ERROR_HANDLER_BACKTRACE_DPM => DEVEL_ERROR_HANDLER_BACKTRACE_DPM,
DEVEL_ERROR_HANDLER_BACKTRACE_KINT => DEVEL_ERROR_HANDLER_BACKTRACE_KINT,
]);
$this->assertOptionSelected('edit-error-handlers', DEVEL_ERROR_HANDLER_BACKTRACE_DPM);
$this->assertOptionSelected('edit-error-handlers', DEVEL_ERROR_HANDLER_BACKTRACE_KINT);
$this->clickLink('notice+warning');
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->assertRawErrorMessage($error_notice);
$this->assertRawErrorMessage($error_warning);
$this->assertErrorMessage($error_notice);
$this->assertErrorMessage($error_warning);
// Ensures that setting the error reporting to all the output produced by
// handlers is shown on the site in case of php errors.
$config->set('error_level', ERROR_REPORTING_DISPLAY_ALL)->save();
$this->clickLink('notice+warning');
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->assertRawErrorMessage($error_notice);
$this->assertRawErrorMessage($error_warning);
$this->assertErrorMessage($error_notice);
$this->assertErrorMessage($error_warning);
// Ensures that setting the error reporting to some the output produced by
// handlers is shown on the site in case of php errors.
$config->set('error_level', ERROR_REPORTING_DISPLAY_SOME)->save();
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->clickLink('notice+warning');
$this->assertRawErrorMessage($error_notice);
$this->assertRawErrorMessage($error_warning);
$this->assertErrorMessage($error_notice);
$this->assertErrorMessage($error_warning);
// Ensures that setting the error reporting to none the output produced by
// handlers is not shown on the site in case of php errors.
$config->set('error_level', ERROR_REPORTING_HIDE)->save();
$this->clickLink('notice+warning');
$this->assertResponse(200, 'Received expected HTTP status code.');
$this->assertNoRawErrorMessage($error_notice);
$this->assertNoRawErrorMessage($error_warning);
$this->assertNoErrorMessage($error_notice);
$this->assertNoErrorMessage($error_warning);
// The errors are expected. Do not interpret them as a test failure.
// Not using File API; a potential error must trigger a PHP warning.
unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log');
}
/**
* Helper function: assert that the error message is found.
*
* @param array $error
* The error to check.
*/
protected function assertRawErrorMessage(array $error) {
$message = new FormattableMarkup('%type: @message in %function (line ', $error);
$this->assertRaw($message, new FormattableMarkup('Found raw error message: @message.', ['@message' => $message]));
}
/**
* Helper function: assert that the error message is not found.
*
*
* @param array $error
* The error to check.
*/
protected function assertNoRawErrorMessage(array $error) {
$message = new FormattableMarkup('%type: @message in %function (line ', $error);
$this->assertNoRaw($message, new FormattableMarkup('Did not find raw error message: @message.', ['@message' => $message]));
}
/**
* Helper function: assert that the error message is found.
*
* @param array $error
* The error to check.
*/
protected function assertErrorMessage(array $error) {
$pattern = '//div[contains(@class, "messages--warning")]//pre[contains(., :content)]';
$message = new FormattableMarkup('%type: @message in %function (line ', $error);
$message = html_entity_decode(strip_tags((string) $message));
$xpath = $this->xpath($pattern, [':content' => $message]);
$this->assertTrue(!empty($xpath), new FormattableMarkup('Found error message: @message.', ['@message' => $message]));
}
/**
* Helper function: assert that the error message is not found.
*
* @param array $error
* The error to check.
*/
protected function assertNoErrorMessage(array $error) {
$pattern = '//div[contains(@class, "messages--warning")]//pre[contains(., :content)]';
$message = new FormattableMarkup('%type: @message in %function (line ', $error);
$message = html_entity_decode(strip_tags((string) $message));
$xpath = $this->xpath($pattern, [':content' => $message]);
$this->assertTrue(empty($xpath), new FormattableMarkup('Found error message: @message.', ['@message' => $message]));
}
}
@@ -1,48 +0,0 @@
<?php
namespace Drupal\devel\Tests;
use Drupal\devel\Plugin\Mail\DevelMailLog;
use Drupal\simpletest\WebTestBase;
/**
* Tests sending mails with debug interface.
*
* @group devel
*/
class DevelMailTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('devel');
protected $profile = 'testing';
/**
* Test mail logging functionality.
*/
public function testDevelMail() {
$message = array();
$message['to'] = 'drupal@example.com';
$message['subject'] = 'Test mail';
$message['headers'] = array(
'From' => 'postmaster@example.com',
'X-stupid' => 'dumb',
);
$message['body'] = "I am the body of this message";
$d = new DevelMailLog();
$filename = $d->getFileName($message);
$content = $d->composeMessage($message);
$expected_filename = $d->getOutputDirectory() . '/drupal@example.com-Test_mail-' . date('y-m-d_his') . '.mail.txt';
$this->assertEqual($filename, $expected_filename);
$content = str_replace("\r", '', $content);
$this->assertEqual($content, 'From: postmaster@example.com
X-stupid: dumb
To: drupal@example.com
Subject: Test mail
I am the body of this message');
}
}
@@ -106,42 +106,4 @@ class DevelMenuLinksTest extends WebTestBase {
$this->assertUrl($url);
}
/**
* Tests menu item link.
*/
public function testMenuItemLink() {
// Ensures that devel menu item works properly.
$url = $this->develUser->toUrl();
$path = '/' . $url->getInternalPath();
$this->drupalGet($url);
$this->clickLink(t('Menu Item'));
$this->assertResponse(200);
$this->assertText('Menu item');
$this->assertUrl('devel/menu/item', ['query' => ['path' => $path]]);
// Ensures that devel menu item works properly even when dynamic cache is
// enabled.
$url = Url::fromRoute('devel.simple_page');
$path = '/' . $url->getInternalPath();
$this->drupalGet($url);
$this->clickLink(t('Menu Item'));
$this->assertResponse(200);
$this->assertText('Menu item');
$this->assertUrl('devel/menu/item', ['query' => ['path' => $path]]);
// Ensures that if no 'path' query string is passed devel menu item does
// not return errors.
$this->drupalGet('devel/menu/item');
$this->assertResponse(200);
$this->assertText('Menu item');
// Ensures that devel menu item is accessible ony to users with the
// adequate permissions.
$this->drupalLogout();
$this->drupalGet('devel/menu/item');
$this->assertResponse(403);
}
}
@@ -1,41 +0,0 @@
<?php
namespace Drupal\devel\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests routes rebuild.
*
* @group devel
*/
class DevelRebuildMenusTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('devel');
/**
* Set up test.
*/
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(array('administer site configuration'));
$this->drupalLogin($web_user);
}
/**
* Test routes rebuild.
*/
public function testDevelRebuildMenus() {
$this->drupalGet('devel/menu/reset');
$this->assertResponse(200);
$this->drupalPostForm('devel/menu/reset', array(), t('Rebuild'));
$this->assertText(t('The menu router has been rebuilt.'));
}
}
@@ -1,210 +0,0 @@
<?php
namespace Drupal\devel\Tests;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\simpletest\WebTestBase;
/**
* Tests devel state editor.
*
* @group devel
*/
class DevelStateEditorTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['devel'];
/**
* The state store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $develUser;
/**
* The user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->state = $this->container->get('state');
$this->develUser = $this->drupalCreateUser(['access devel information']);
$this->adminUser = $this->drupalCreateUser(['access devel information', 'administer site configuration']);
}
/**
* Tests state listing.
*/
public function testStateListing() {
// Ensure that state listing page is accessible only by users with the
// adequate permissions.
$this->drupalGet('devel/state');
$this->assertResponse(403);
$this->drupalLogin($this->develUser);
$this->drupalGet('devel/state');
$this->assertResponse(200);
$this->assertText(t('State editor'));
// Ensure that the state variables table is visible.
$table = $this->xpath('//table[contains(@class, "devel-state-list")]');
$this->assertTrue($table, 'State list table found.');
// Ensure that all state variables are listed in the table.
$states = \Drupal::keyValue('state')->getAll();
$rows = $this->xpath('//table[contains(@class, "devel-state-list")]//tbody//tr');
$this->assertEqual(count($rows), count($states), 'All states are listed in the table.');
// Ensure that the added state variables are listed in the table.
$this->state->set('devel.simple', 'Hello!');
$this->drupalGet('devel/state');
$this->assertFieldByXpath('//table[contains(@class, "devel-state-list")]//tbody//td', 'devel.simple', 'Label found for the added state.');
$thead_xpath = '//table[contains(@class, "devel-state-list")]/thead/tr/th';
$action_xpath = '//table[contains(@class, "devel-state-list")]//ul[@class="dropbutton"]/li/a';
// Ensure that the operations column and the actions buttons are not
// available for user without 'administer site configuration' permission.
$elements = $this->xpath($thead_xpath);
$this->assertEqual(count($elements), 2, 'Correct number of table header cells found.');
$expected_items = ['Name', 'Value'];
foreach ($elements as $key => $element) {
$this->assertIdentical((string) $element[0], $expected_items[$key]);
}
$this->assertFalse($this->xpath($action_xpath), 'Action buttons are not visible.');
// Ensure that the operations column and the actions buttons are
// available for user with 'administer site configuration' permission.
$this->drupalLogin($this->adminUser);
$this->drupalGet('devel/state');
$elements = $this->xpath($thead_xpath);
$this->assertEqual(count($elements), 3, 'Correct number of table header cells found.');
$expected_items = ['Name', 'Value', 'Operations'];
foreach ($elements as $key => $element) {
$this->assertIdentical((string) $element[0], $expected_items[$key]);
}
$this->assertTrue($this->xpath($action_xpath), 'Action buttons are visible.');
// Test that the edit button works properly.
$this->clickLink(t('Edit'));
$this->assertResponse(200);
}
/**
* Tests state edit.
*/
public function testStateEdit() {
// Create some state variables for the test.
$this->state->set('devel.simple', 0);
$this->state->set('devel.array', ['devel' => 'value']);
$this->state->set('devel.object', $this->randomObject());
// Ensure that state edit form is accessible only by users with the
// adequate permissions.
$this->drupalLogin($this->develUser);
$this->drupalGet('devel/state/edit/devel.simple');
$this->assertResponse(403);
$this->drupalLogin($this->adminUser);
// Ensure that accessing an un-existent state variable cause a warning
// message.
$this->drupalGet('devel/state/edit/devel.unknown');
$this->assertText(t('State @name does not exist in the system.', ['@name' => 'devel.unknown']));
// Ensure that state variables that contain simple type can be edited and
// saved.
$this->drupalGet('devel/state/edit/devel.simple');
$this->assertResponse(200);
$this->assertText(t('Edit state variable: @name', ['@name' => 'devel.simple']));
$this->assertInputNotDisabledById('edit-new-value');
$this->assertInputNotDisabledById('edit-submit');
$edit = ['new_value' => 1];
$this->drupalPostForm('devel/state/edit/devel.simple', $edit, t('Save'));
$this->assertText(t('Variable @name was successfully edited.', ['@name' => 'devel.simple']));
$this->assertEqual(1, $this->state->get('devel.simple'));
// Ensure that state variables that contain array can be edited and saved
// and the new value is properly validated.
$this->drupalGet('devel/state/edit/devel.array');
$this->assertResponse(200);
$this->assertText(t('Edit state variable: @name', ['@name' => 'devel.array']));
$this->assertInputNotDisabledById('edit-new-value');
$this->assertInputNotDisabledById('edit-submit');
// Try to save an invalid yaml input.
$edit = ['new_value' => 'devel: \'value updated'];
$this->drupalPostForm('devel/state/edit/devel.array', $edit, t('Save'));
$this->assertText(t('Invalid input:'));
$edit = ['new_value' => 'devel: \'value updated\''];
$this->drupalPostForm('devel/state/edit/devel.array', $edit, t('Save'));
$this->assertText(t('Variable @name was successfully edited.', ['@name' => 'devel.array']));
$this->assertEqual(['devel' => 'value updated'], $this->state->get('devel.array'));
// Ensure that state variables that contain objects cannot be edited.
$this->drupalGet('devel/state/edit/devel.object');
$this->assertResponse(200);
$this->assertText(t('Edit state variable: @name', ['@name' => 'devel.object']));
$this->assertText(t('Only simple structures are allowed to be edited. State @name contains objects.', ['@name' => 'devel.object']));
$this->assertInputDisabledById('edit-new-value');
$this->assertInputDisabledById('edit-submit');
// Ensure that the cancel link works as expected.
$this->clickLink(t('Cancel'));
$this->assertUrl('devel/state');
}
/**
* Helper function for check if an input is disabled.
*
* @param string $id
* The ID of the input.
*/
protected function assertInputDisabledById($id) {
$message = new FormattableMarkup('The input %id is disabled.', ['%id' => $id]);
$xpath = '//textarea[@id=:id and @disabled="disabled"]|//input[@id=:id and @disabled="disabled"]|//select[@id=:id and @disabled="disabled"]';
$query = $this->buildXPathQuery($xpath, [':id' => $id]);
$this->assertFieldByXPath($query, NULL, $message);
}
/**
* Helper function for check if an input is not disabled.
*
* @param string $id
* The ID of the input.
*/
protected function assertInputNotDisabledById($id) {
$message = new FormattableMarkup('The input %id is not disabled.', ['%id' => $id]);
$xpath = '//textarea[@id=:id and not(@disabled="disabled")]|//input[@id=:id and not(@disabled="disabled")]|//select[@id=:id and not(@disabled="disabled")]';
$query = $this->buildXPathQuery($xpath, [':id' => $id]);
$this->assertFieldByXPath($query, NULL, $message);
}
}
@@ -70,7 +70,7 @@ class DevelSwitchUserTest extends WebTestBase {
$this->assertNoText($this->block->label(), 'Block title was not found.');
// Ensure that a token is required to switch user.
$this->drupalGet('/devel/switch/' . $this->webUser->getUsername());
$this->drupalGet('/devel/switch/' . $this->webUser->getDisplayName());
$this->assertResponse(403);
$this->drupalLogin($this->develUser);
@@ -83,22 +83,22 @@ class DevelSwitchUserTest extends WebTestBase {
$this->assertResponse(403);
// Ensure that a token is required to switch user.
$this->drupalGet('/devel/switch/' . $this->switchUser->getUsername());
$this->drupalGet('/devel/switch/' . $this->switchUser->getDisplayName());
$this->assertResponse(403);
// Switch to another user account.
$this->drupalGet('/user/' . $this->switchUser->id());
$this->clickLink($this->switchUser->getUsername());
$this->clickLink($this->switchUser->getDisplayName());
$this->assertSessionByUid($this->switchUser->id());
$this->assertNoSessionByUid($this->develUser->id());
// Switch back to initial account.
$this->clickLink($this->develUser->getUsername());
$this->clickLink($this->develUser->getDisplayName());
$this->assertNoSessionByUid($this->switchUser->id());
$this->assertSessionByUid($this->develUser->id());
// Use the search form to switch to another account.
$edit = ['userid' => $this->switchUser->getUsername()];
$edit = ['userid' => $this->switchUser->getDisplayName()];
$this->drupalPostForm(NULL, $edit, t('Switch'));
$this->assertSessionByUid($this->switchUser->id());
$this->assertNoSessionByUid($this->develUser->id());
@@ -166,22 +166,22 @@ class DevelSwitchUserTest extends WebTestBase {
// Ensure that user with 'switch users' permission are prioritized.
$this->assertSwitchUserListCount(2);
$this->assertSwitchUserListContainsUser($this->develUser->getUsername());
$this->assertSwitchUserListContainsUser($this->switchUser->getUsername());
$this->assertSwitchUserListContainsUser($this->develUser->getDisplayName());
$this->assertSwitchUserListContainsUser($this->switchUser->getDisplayName());
// Ensure that blocked users are not shown in the list.
$this->switchUser->set('status', 0)->save();
$this->drupalGet('');
$this->assertSwitchUserListCount(2);
$this->assertSwitchUserListContainsUser($this->develUser->getUsername());
$this->assertSwitchUserListContainsUser($this->webUser->getUsername());
$this->assertSwitchUserListNoContainsUser($this->switchUser->getUsername());
$this->assertSwitchUserListContainsUser($this->develUser->getDisplayName());
$this->assertSwitchUserListContainsUser($this->webUser->getDisplayName());
$this->assertSwitchUserListNoContainsUser($this->switchUser->getDisplayName());
// Ensure that anonymous user are prioritized if include_anon is set to true.
$this->setBlockConfiguration('include_anon', TRUE);
$this->drupalGet('');
$this->assertSwitchUserListCount(2);
$this->assertSwitchUserListContainsUser($this->develUser->getUsername());
$this->assertSwitchUserListContainsUser($this->develUser->getDisplayName());
$this->assertSwitchUserListContainsUser($anonymous);
// Ensure that the switch user block works properly even if no prioritized
@@ -192,7 +192,7 @@ class DevelSwitchUserTest extends WebTestBase {
$this->drupalLogin($this->rootUser);
$this->drupalGet('');
$this->assertSwitchUserListCount(2);
$this->assertSwitchUserListContainsUser($this->rootUser->getUsername());
$this->assertSwitchUserListContainsUser($this->rootUser->getDisplayName());
$this->assertSwitchUserListContainsUser($anonymous);
// Ensure that the switch user block works properly even if no roles have
@@ -202,7 +202,7 @@ class DevelSwitchUserTest extends WebTestBase {
$this->drupalGet('');
$this->assertSwitchUserListCount(2);
$this->assertSwitchUserListContainsUser($this->rootUser->getUsername());
$this->assertSwitchUserListContainsUser($this->rootUser->getDisplayName());
$this->assertSwitchUserListContainsUser($anonymous);
}
@@ -0,0 +1,181 @@
<?php
namespace Drupal\devel;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Toolbar integration handler.
*/
class ToolbarHandler implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuLinkTree;
/**
* The devel toolbar config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $account;
/**
* ToolbarHandler constructor.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
*/
public function __construct(MenuLinkTreeInterface $menu_link_tree, ConfigFactoryInterface $config_factory, AccountProxyInterface $account) {
$this->menuLinkTree = $menu_link_tree;
$this->config = $config_factory->get('devel.toolbar.settings');
$this->account = $account;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('toolbar.menu_tree'),
$container->get('config.factory'),
$container->get('current_user')
);
}
/**
* Hook bridge.
*
* @return array
* The devel toolbar items render array.
*
* @see hook_toolbar()
*/
public function toolbar() {
$items['devel'] = [
'#cache' => [
'contexts' => ['user.permissions'],
],
];
if ($this->account->hasPermission('access devel information')) {
$items['devel'] += [
'#type' => 'toolbar_item',
'#weight' => 999,
'tab' => [
'#type' => 'link',
'#title' => $this->t('Devel'),
'#url' => Url::fromRoute('devel.admin_settings'),
'#attributes' => [
'title' => $this->t('Development menu'),
'class' => ['toolbar-icon', 'toolbar-icon-devel'],
],
],
'tray' => [
'#heading' => $this->t('Development menu'),
'devel_menu' => [
// Currently devel menu is uncacheable, so instead of poisoning the
// entire page cache we use a lazy builder.
// @see \Drupal\devel\Plugin\Menu\DestinationMenuLink
// @see \Drupal\devel\Plugin\Menu\RouteDetailMenuItem
'#lazy_builder' => [ToolbarHandler::class . ':lazyBuilder', []],
// Force the creation of the placeholder instead of rely on the
// automatical placeholdering or otherwise the page results
// uncacheable when max-age 0 is bubbled up.
'#create_placeholder' => TRUE,
],
'configuration' => [
'#type' => 'link',
'#title' => $this->t('Configure'),
'#url' => Url::fromRoute('devel.toolbar.settings_form'),
'#options' => [
'attributes' => ['class' => ['edit-devel-toolbar']],
],
],
],
'#attached' => [
'library' => 'devel/devel-toolbar',
],
];
}
return $items;
}
/**
* Lazy builder callback for the devel menu toolbar.
*
* @return array
* The renderable array rapresentation of the devel menu.
*/
public function lazyBuilder() {
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = $this->menuLinkTree->load('devel', $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
['callable' => ToolbarHandler::class . ':processTree'],
];
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$build = $this->menuLinkTree->build($tree);
CacheableMetadata::createFromRenderArray($build)
->addCacheableDependency($this->config)
->applyTo($build);
return $build;
}
/**
* Adds toolbar-specific attributes to the menu link tree.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function processTree(array $tree) {
$visible_items = $this->config->get('toolbar_items') ?: [];
foreach ($tree as $element) {
$plugin_id = $element->link->getPluginId();
if (!in_array($plugin_id, $visible_items)) {
// Add a class that allow to hide the non prioritized menu items when
// the toolbar has horizontal orientation.
$element->options['attributes']['class'][] = 'toolbar-horizontal-item-hidden';
}
}
return $tree;
}
}