BlockForm.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <?php
  2. namespace Drupal\block;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Core\Plugin\PluginFormFactoryInterface;
  5. use Drupal\Core\Block\BlockPluginInterface;
  6. use Drupal\Core\Entity\EntityForm;
  7. use Drupal\Core\Entity\EntityManagerInterface;
  8. use Drupal\Core\Executable\ExecutableManagerInterface;
  9. use Drupal\Core\Extension\ThemeHandlerInterface;
  10. use Drupal\Core\Form\FormStateInterface;
  11. use Drupal\Core\Form\SubformState;
  12. use Drupal\Core\Language\LanguageManagerInterface;
  13. use Drupal\Core\Plugin\ContextAwarePluginInterface;
  14. use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
  15. use Drupal\Core\Plugin\PluginWithFormsInterface;
  16. use Symfony\Component\DependencyInjection\ContainerInterface;
  17. /**
  18. * Provides form for block instance forms.
  19. *
  20. * @internal
  21. */
  22. class BlockForm extends EntityForm {
  23. /**
  24. * The block entity.
  25. *
  26. * @var \Drupal\block\BlockInterface
  27. */
  28. protected $entity;
  29. /**
  30. * The block storage.
  31. *
  32. * @var \Drupal\Core\Entity\EntityStorageInterface
  33. */
  34. protected $storage;
  35. /**
  36. * The condition plugin manager.
  37. *
  38. * @var \Drupal\Core\Condition\ConditionManager
  39. */
  40. protected $manager;
  41. /**
  42. * The event dispatcher service.
  43. *
  44. * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
  45. */
  46. protected $dispatcher;
  47. /**
  48. * The language manager service.
  49. *
  50. * @var \Drupal\Core\Language\LanguageManagerInterface
  51. */
  52. protected $language;
  53. /**
  54. * The theme handler.
  55. *
  56. * @var \Drupal\Core\Extension\ThemeHandler
  57. */
  58. protected $themeHandler;
  59. /**
  60. * The context repository service.
  61. *
  62. * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
  63. */
  64. protected $contextRepository;
  65. /**
  66. * The plugin form manager.
  67. *
  68. * @var \Drupal\Core\Plugin\PluginFormFactoryInterface
  69. */
  70. protected $pluginFormFactory;
  71. /**
  72. * Constructs a BlockForm object.
  73. *
  74. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
  75. * The entity manager.
  76. * @param \Drupal\Core\Executable\ExecutableManagerInterface $manager
  77. * The ConditionManager for building the visibility UI.
  78. * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
  79. * The lazy context repository service.
  80. * @param \Drupal\Core\Language\LanguageManagerInterface $language
  81. * The language manager.
  82. * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
  83. * The theme handler.
  84. * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
  85. * The plugin form manager.
  86. */
  87. public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, PluginFormFactoryInterface $plugin_form_manager) {
  88. $this->storage = $entity_manager->getStorage('block');
  89. $this->manager = $manager;
  90. $this->contextRepository = $context_repository;
  91. $this->language = $language;
  92. $this->themeHandler = $theme_handler;
  93. $this->pluginFormFactory = $plugin_form_manager;
  94. }
  95. /**
  96. * {@inheritdoc}
  97. */
  98. public static function create(ContainerInterface $container) {
  99. return new static(
  100. $container->get('entity.manager'),
  101. $container->get('plugin.manager.condition'),
  102. $container->get('context.repository'),
  103. $container->get('language_manager'),
  104. $container->get('theme_handler'),
  105. $container->get('plugin_form.factory')
  106. );
  107. }
  108. /**
  109. * {@inheritdoc}
  110. */
  111. public function form(array $form, FormStateInterface $form_state) {
  112. $entity = $this->entity;
  113. // Store theme settings in $form_state for use below.
  114. if (!$theme = $entity->getTheme()) {
  115. $theme = $this->config('system.theme')->get('default');
  116. }
  117. $form_state->set('block_theme', $theme);
  118. // Store the gathered contexts in the form state for other objects to use
  119. // during form building.
  120. $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
  121. $form['#tree'] = TRUE;
  122. $form['settings'] = [];
  123. $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
  124. $form['settings'] = $this->getPluginForm($entity->getPlugin())->buildConfigurationForm($form['settings'], $subform_state);
  125. $form['visibility'] = $this->buildVisibilityInterface([], $form_state);
  126. // If creating a new block, calculate a safe default machine name.
  127. $form['id'] = [
  128. '#type' => 'machine_name',
  129. '#maxlength' => 64,
  130. '#description' => $this->t('A unique name for this block instance. Must be alpha-numeric and underscore separated.'),
  131. '#default_value' => !$entity->isNew() ? $entity->id() : $this->getUniqueMachineName($entity),
  132. '#machine_name' => [
  133. 'exists' => '\Drupal\block\Entity\Block::load',
  134. 'replace_pattern' => '[^a-z0-9_.]+',
  135. 'source' => ['settings', 'label'],
  136. ],
  137. '#required' => TRUE,
  138. '#disabled' => !$entity->isNew(),
  139. ];
  140. // Theme settings.
  141. if ($entity->getTheme()) {
  142. $form['theme'] = [
  143. '#type' => 'value',
  144. '#value' => $theme,
  145. ];
  146. }
  147. else {
  148. $theme_options = [];
  149. foreach ($this->themeHandler->listInfo() as $theme_name => $theme_info) {
  150. if (!empty($theme_info->status)) {
  151. $theme_options[$theme_name] = $theme_info->info['name'];
  152. }
  153. }
  154. $form['theme'] = [
  155. '#type' => 'select',
  156. '#options' => $theme_options,
  157. '#title' => t('Theme'),
  158. '#default_value' => $theme,
  159. '#ajax' => [
  160. 'callback' => '::themeSwitch',
  161. 'wrapper' => 'edit-block-region-wrapper',
  162. ],
  163. ];
  164. }
  165. // Hidden weight setting.
  166. $weight = $entity->isNew() ? $this->getRequest()->query->get('weight', 0) : $entity->getWeight();
  167. $form['weight'] = [
  168. '#type' => 'hidden',
  169. '#default_value' => $weight,
  170. ];
  171. // Region settings.
  172. $entity_region = $entity->getRegion();
  173. $region = $entity->isNew() ? $this->getRequest()->query->get('region', $entity_region) : $entity_region;
  174. $form['region'] = [
  175. '#type' => 'select',
  176. '#title' => $this->t('Region'),
  177. '#description' => $this->t('Select the region where this block should be displayed.'),
  178. '#default_value' => $region,
  179. '#required' => TRUE,
  180. '#options' => system_region_list($theme, REGIONS_VISIBLE),
  181. '#prefix' => '<div id="edit-block-region-wrapper">',
  182. '#suffix' => '</div>',
  183. ];
  184. $form['#attached']['library'][] = 'block/drupal.block.admin';
  185. return $form;
  186. }
  187. /**
  188. * Handles switching the available regions based on the selected theme.
  189. */
  190. public function themeSwitch($form, FormStateInterface $form_state) {
  191. $form['region']['#options'] = system_region_list($form_state->getValue('theme'), REGIONS_VISIBLE);
  192. return $form['region'];
  193. }
  194. /**
  195. * Helper function for building the visibility UI form.
  196. *
  197. * @param array $form
  198. * An associative array containing the structure of the form.
  199. * @param \Drupal\Core\Form\FormStateInterface $form_state
  200. * The current state of the form.
  201. *
  202. * @return array
  203. * The form array with the visibility UI added in.
  204. */
  205. protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) {
  206. $form['visibility_tabs'] = [
  207. '#type' => 'vertical_tabs',
  208. '#title' => $this->t('Visibility'),
  209. '#parents' => ['visibility_tabs'],
  210. '#attached' => [
  211. 'library' => [
  212. 'block/drupal.block',
  213. ],
  214. ],
  215. ];
  216. // @todo Allow list of conditions to be configured in
  217. // https://www.drupal.org/node/2284687.
  218. $visibility = $this->entity->getVisibility();
  219. $definitions = $this->manager->getFilteredDefinitions('block_ui', $form_state->getTemporaryValue('gathered_contexts'), ['block' => $this->entity]);
  220. foreach ($definitions as $condition_id => $definition) {
  221. // Don't display the current theme condition.
  222. if ($condition_id == 'current_theme') {
  223. continue;
  224. }
  225. // Don't display the language condition until we have multiple languages.
  226. if ($condition_id == 'language' && !$this->language->isMultilingual()) {
  227. continue;
  228. }
  229. /** @var \Drupal\Core\Condition\ConditionInterface $condition */
  230. $condition = $this->manager->createInstance($condition_id, isset($visibility[$condition_id]) ? $visibility[$condition_id] : []);
  231. $form_state->set(['conditions', $condition_id], $condition);
  232. $condition_form = $condition->buildConfigurationForm([], $form_state);
  233. $condition_form['#type'] = 'details';
  234. $condition_form['#title'] = $condition->getPluginDefinition()['label'];
  235. $condition_form['#group'] = 'visibility_tabs';
  236. $form[$condition_id] = $condition_form;
  237. }
  238. if (isset($form['node_type'])) {
  239. $form['node_type']['#title'] = $this->t('Content types');
  240. $form['node_type']['bundles']['#title'] = $this->t('Content types');
  241. $form['node_type']['negate']['#type'] = 'value';
  242. $form['node_type']['negate']['#title_display'] = 'invisible';
  243. $form['node_type']['negate']['#value'] = $form['node_type']['negate']['#default_value'];
  244. }
  245. if (isset($form['user_role'])) {
  246. $form['user_role']['#title'] = $this->t('Roles');
  247. unset($form['user_role']['roles']['#description']);
  248. $form['user_role']['negate']['#type'] = 'value';
  249. $form['user_role']['negate']['#value'] = $form['user_role']['negate']['#default_value'];
  250. }
  251. if (isset($form['request_path'])) {
  252. $form['request_path']['#title'] = $this->t('Pages');
  253. $form['request_path']['negate']['#type'] = 'radios';
  254. $form['request_path']['negate']['#default_value'] = (int) $form['request_path']['negate']['#default_value'];
  255. $form['request_path']['negate']['#title_display'] = 'invisible';
  256. $form['request_path']['negate']['#options'] = [
  257. $this->t('Show for the listed pages'),
  258. $this->t('Hide for the listed pages'),
  259. ];
  260. }
  261. if (isset($form['language'])) {
  262. $form['language']['negate']['#type'] = 'value';
  263. $form['language']['negate']['#value'] = $form['language']['negate']['#default_value'];
  264. }
  265. return $form;
  266. }
  267. /**
  268. * {@inheritdoc}
  269. */
  270. protected function actions(array $form, FormStateInterface $form_state) {
  271. $actions = parent::actions($form, $form_state);
  272. $actions['submit']['#value'] = $this->t('Save block');
  273. $actions['delete']['#title'] = $this->t('Remove block');
  274. return $actions;
  275. }
  276. /**
  277. * {@inheritdoc}
  278. */
  279. public function validateForm(array &$form, FormStateInterface $form_state) {
  280. parent::validateForm($form, $form_state);
  281. $form_state->setValue('weight', (int) $form_state->getValue('weight'));
  282. // The Block Entity form puts all block plugin form elements in the
  283. // settings form element, so just pass that to the block for validation.
  284. $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
  285. $this->validateVisibility($form, $form_state);
  286. }
  287. /**
  288. * Helper function to independently validate the visibility UI.
  289. *
  290. * @param array $form
  291. * A nested array form elements comprising the form.
  292. * @param \Drupal\Core\Form\FormStateInterface $form_state
  293. * The current state of the form.
  294. */
  295. protected function validateVisibility(array $form, FormStateInterface $form_state) {
  296. // Validate visibility condition settings.
  297. foreach ($form_state->getValue('visibility') as $condition_id => $values) {
  298. // All condition plugins use 'negate' as a Boolean in their schema.
  299. // However, certain form elements may return it as 0/1. Cast here to
  300. // ensure the data is in the expected type.
  301. if (array_key_exists('negate', $values)) {
  302. $form_state->setValue(['visibility', $condition_id, 'negate'], (bool) $values['negate']);
  303. }
  304. // Allow the condition to validate the form.
  305. $condition = $form_state->get(['conditions', $condition_id]);
  306. $condition->validateConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
  307. }
  308. }
  309. /**
  310. * {@inheritdoc}
  311. */
  312. public function submitForm(array &$form, FormStateInterface $form_state) {
  313. parent::submitForm($form, $form_state);
  314. $entity = $this->entity;
  315. // The Block Entity form puts all block plugin form elements in the
  316. // settings form element, so just pass that to the block for submission.
  317. $sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state);
  318. // Call the plugin submit handler.
  319. $block = $entity->getPlugin();
  320. $this->getPluginForm($block)->submitConfigurationForm($form, $sub_form_state);
  321. // If this block is context-aware, set the context mapping.
  322. if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) {
  323. $context_mapping = $sub_form_state->getValue('context_mapping', []);
  324. $block->setContextMapping($context_mapping);
  325. }
  326. $this->submitVisibility($form, $form_state);
  327. // Save the settings of the plugin.
  328. $entity->save();
  329. $this->messenger()->addStatus($this->t('The block configuration has been saved.'));
  330. $form_state->setRedirect(
  331. 'block.admin_display_theme',
  332. [
  333. 'theme' => $form_state->getValue('theme'),
  334. ],
  335. ['query' => ['block-placement' => Html::getClass($this->entity->id())]]
  336. );
  337. }
  338. /**
  339. * Helper function to independently submit the visibility UI.
  340. *
  341. * @param array $form
  342. * A nested array form elements comprising the form.
  343. * @param \Drupal\Core\Form\FormStateInterface $form_state
  344. * The current state of the form.
  345. */
  346. protected function submitVisibility(array $form, FormStateInterface $form_state) {
  347. foreach ($form_state->getValue('visibility') as $condition_id => $values) {
  348. // Allow the condition to submit the form.
  349. $condition = $form_state->get(['conditions', $condition_id]);
  350. $condition->submitConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
  351. // Setting conditions' context mappings is the plugins' responsibility.
  352. // This code exists for backwards compatibility, because
  353. // \Drupal\Core\Condition\ConditionPluginBase::submitConfigurationForm()
  354. // did not set its own mappings until Drupal 8.2
  355. // @todo Remove the code that sets context mappings in Drupal 9.0.0.
  356. if ($condition instanceof ContextAwarePluginInterface) {
  357. $context_mapping = isset($values['context_mapping']) ? $values['context_mapping'] : [];
  358. $condition->setContextMapping($context_mapping);
  359. }
  360. $condition_configuration = $condition->getConfiguration();
  361. // Update the visibility conditions on the block.
  362. $this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
  363. }
  364. }
  365. /**
  366. * Generates a unique machine name for a block.
  367. *
  368. * @param \Drupal\block\BlockInterface $block
  369. * The block entity.
  370. *
  371. * @return string
  372. * Returns the unique name.
  373. */
  374. public function getUniqueMachineName(BlockInterface $block) {
  375. $suggestion = $block->getPlugin()->getMachineNameSuggestion();
  376. // Get all the blocks which starts with the suggested machine name.
  377. $query = $this->storage->getQuery();
  378. $query->condition('id', $suggestion, 'CONTAINS');
  379. $block_ids = $query->execute();
  380. $block_ids = array_map(function ($block_id) {
  381. $parts = explode('.', $block_id);
  382. return end($parts);
  383. }, $block_ids);
  384. // Iterate through potential IDs until we get a new one. E.g.
  385. // 'plugin', 'plugin_2', 'plugin_3', etc.
  386. $count = 1;
  387. $machine_default = $suggestion;
  388. while (in_array($machine_default, $block_ids)) {
  389. $machine_default = $suggestion . '_' . ++$count;
  390. }
  391. return $machine_default;
  392. }
  393. /**
  394. * Retrieves the plugin form for a given block and operation.
  395. *
  396. * @param \Drupal\Core\Block\BlockPluginInterface $block
  397. * The block plugin.
  398. *
  399. * @return \Drupal\Core\Plugin\PluginFormInterface
  400. * The plugin form for the block.
  401. */
  402. protected function getPluginForm(BlockPluginInterface $block) {
  403. if ($block instanceof PluginWithFormsInterface) {
  404. return $this->pluginFormFactory->createInstance($block, 'configure');
  405. }
  406. return $block;
  407. }
  408. }