FeaturesExportForm.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. <?php
  2. namespace Drupal\features_ui\Form;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Component\Utility\Xss;
  5. use Drupal\features\FeaturesAssignerInterface;
  6. use Drupal\features\FeaturesGeneratorInterface;
  7. use Drupal\features\FeaturesManagerInterface;
  8. use Drupal\Core\Extension\ModuleHandlerInterface;
  9. use Drupal\Core\Form\FormBase;
  10. use Drupal\Core\Form\FormStateInterface;
  11. use Drupal\Core\Render\Element;
  12. use Drupal\features\Package;
  13. use Symfony\Component\DependencyInjection\ContainerInterface;
  14. use Drupal\Core\Url;
  15. /**
  16. * Defines the configuration export form.
  17. */
  18. class FeaturesExportForm extends FormBase {
  19. /**
  20. * The features manager.
  21. *
  22. * @var array
  23. */
  24. protected $featuresManager;
  25. /**
  26. * The package assigner.
  27. *
  28. * @var array
  29. */
  30. protected $assigner;
  31. /**
  32. * The package generator.
  33. *
  34. * @var array
  35. */
  36. protected $generator;
  37. /**
  38. * The module handler service.
  39. *
  40. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  41. */
  42. protected $moduleHandler;
  43. /**
  44. * Constructs a FeaturesExportForm object.
  45. *
  46. * @param \Drupal\features\FeaturesManagerInterface $features_manager
  47. * The features manager.
  48. * @param \Drupal\features\FeaturesAssignerInterface $features_assigner
  49. * The features assigner.
  50. * @param \Drupal\features\FeaturesGeneratorInterface $features_generator
  51. * The features generator.
  52. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  53. * The features generator.
  54. */
  55. public function __construct(FeaturesManagerInterface $features_manager, FeaturesAssignerInterface $assigner, FeaturesGeneratorInterface $generator, ModuleHandlerInterface $module_handler) {
  56. $this->featuresManager = $features_manager;
  57. $this->assigner = $assigner;
  58. $this->generator = $generator;
  59. $this->moduleHandler = $module_handler;
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. public static function create(ContainerInterface $container) {
  65. return new static(
  66. $container->get('features.manager'),
  67. $container->get('features_assigner'),
  68. $container->get('features_generator'),
  69. $container->get('module_handler')
  70. );
  71. }
  72. /**
  73. * {@inheritdoc}
  74. */
  75. public function getFormId() {
  76. return 'features_export_form';
  77. }
  78. /**
  79. * Detects if an element triggered the form submission via Ajax.
  80. * TODO: SHOULDN'T NEED THIS! BUT DRUPAL IS CALLING buildForm AFTER THE
  81. * BUNDLE AJAX IS SELECTED AND DOESN'T HAVE getTriggeringElement() SET YET.
  82. */
  83. protected function elementTriggeredScriptedSubmission(FormStateInterface &$form_state) {
  84. $input = $form_state->getUserInput();
  85. if (!empty($input['_triggering_element_name'])) {
  86. return $input['_triggering_element_name'];
  87. }
  88. return '';
  89. }
  90. /**
  91. * {@inheritdoc}
  92. */
  93. public function buildForm(array $form, FormStateInterface $form_state) {
  94. $trigger = $form_state->getTriggeringElement();
  95. // TODO: See if there is a Drupal Core issue for this.
  96. // Sometimes the first ajax call on the page causes buildForm to be called
  97. // twice! First time form_state->getTriggeringElement is NOT SET, but
  98. // the form_state['input'] shows the _triggering_element_name. Then the
  99. // SECOND time it is called the getTriggeringElement is fine.
  100. $real_trigger = $this->elementTriggeredScriptedSubmission($form_state);
  101. if (!isset($trigger) && ($real_trigger == 'bundle')) {
  102. $input = $form_state->getUserInput();
  103. $bundle_name = $input['bundle'];
  104. $this->assigner->setCurrent($this->assigner->getBundle($bundle_name));
  105. }
  106. elseif ($trigger['#name'] == 'bundle') {
  107. $bundle_name = $form_state->getValue('bundle', '');
  108. $this->assigner->setCurrent($this->assigner->getBundle($bundle_name));
  109. }
  110. else {
  111. $this->assigner->loadBundle();
  112. }
  113. $current_bundle = $this->assigner->getBundle();
  114. $this->assigner->assignConfigPackages();
  115. $packages = $this->featuresManager->getPackages();
  116. $config_collection = $this->featuresManager->getConfigCollection();
  117. // Add in un-packaged configuration items.
  118. $this->addUnpackaged($packages, $config_collection);
  119. // Filter packages on bundle if selected.
  120. if (!$current_bundle->isDefault()) {
  121. $packages = $this->featuresManager->filterPackages($packages, $current_bundle->getMachineName(), TRUE);
  122. }
  123. // Pass the packages and bundle data for use in the form pre_render
  124. // callback.
  125. $form['#packages'] = $packages;
  126. $form['#profile_package'] = $current_bundle->getProfileName();
  127. $form['header'] = array(
  128. '#type' => 'container',
  129. '#attributes' => array('class' => 'features-header'),
  130. );
  131. $bundle_options = $this->assigner->getBundleOptions();
  132. // If there are no custom bundles, provide message.
  133. if (count($bundle_options) < 2) {
  134. drupal_set_message($this->t('You have not yet created any bundles. Before generating features, you may wish to <a href=":create">create a bundle</a> to group your features within.', [':create' => Url::fromRoute('features.assignment')->toString()]));
  135. }
  136. $form['#prefix'] = '<div id="edit-features-wrapper">';
  137. $form['#suffix'] = '</div>';
  138. $form['header']['bundle'] = array(
  139. '#title' => $this->t('Bundle'),
  140. '#type' => 'select',
  141. '#options' => $bundle_options,
  142. '#default_value' => $current_bundle->getMachineName(),
  143. '#prefix' => '<div id="edit-package-set-wrapper">',
  144. '#suffix' => '</div>',
  145. '#ajax' => array(
  146. 'callback' => '::updatePreview',
  147. 'wrapper' => 'edit-features-preview-wrapper',
  148. ),
  149. '#attributes' => array(
  150. 'data-new-package-set' => 'status',
  151. ),
  152. );
  153. $form['preview'] = $this->buildListing($packages);
  154. $form['#attached'] = array(
  155. 'library' => array(
  156. 'features_ui/drupal.features_ui.admin',
  157. ),
  158. );
  159. if (\Drupal::currentUser()->hasPermission('export configuration')) {
  160. // Offer available generation methods.
  161. $generation_info = $this->generator->getGenerationMethods();
  162. // Sort generation methods by weight.
  163. uasort($generation_info, '\Drupal\Component\Utility\SortArray::sortByWeightElement');
  164. $form['description'] = array(
  165. '#markup' => '<p>' . $this->t('Use an export method button below to generate the selected features.') . '</p>',
  166. );
  167. $form['actions'] = array('#type' => 'actions', '#tree' => TRUE);
  168. foreach ($generation_info as $method_id => $method) {
  169. $form['actions'][$method_id] = array(
  170. '#type' => 'submit',
  171. '#name' => $method_id,
  172. '#value' => $this->t('@name', array('@name' => $method['name'])),
  173. '#attributes' => array(
  174. 'title' => Html::escape($method['description']),
  175. ),
  176. );
  177. }
  178. }
  179. $form['#pre_render'][] = array(get_class($this), 'preRenderRemoveInvalidCheckboxes');
  180. return $form;
  181. }
  182. /**
  183. * Handles switching the configuration type selector.
  184. */
  185. public function updatePreview($form, FormStateInterface $form_state) {
  186. // We should really be able to add this pre_render callback to the
  187. // 'preview' element. However, since doing so leads to an error (no rows
  188. // are displayed), we need to instead explicitly invoke it here for the
  189. // processing to apply to the Ajax-rendered form element.
  190. $form = $this->preRenderRemoveInvalidCheckboxes($form);
  191. return $form['preview'];
  192. }
  193. /**
  194. * Builds the portion of the form showing a listing of features.
  195. *
  196. * @param \Drupal\features\Package[] $packages
  197. * The packages.
  198. *
  199. * @return array
  200. * A render array of a form element.
  201. */
  202. protected function buildListing(array $packages) {
  203. $header = array(
  204. 'name' => array('data' => $this->t('Feature')),
  205. 'machine_name' => array('data' => $this->t('')),
  206. 'details' => array('data' => $this->t('Description'), 'class' => array(RESPONSIVE_PRIORITY_LOW)),
  207. 'version' => array('data' => $this->t('Version'), 'class' => array(RESPONSIVE_PRIORITY_LOW)),
  208. 'status' => array('data' => $this->t('Status'), 'class' => array(RESPONSIVE_PRIORITY_LOW)),
  209. 'state' => array('data' => $this->t('State'), 'class' => array(RESPONSIVE_PRIORITY_LOW)),
  210. );
  211. $options = array();
  212. $first = TRUE;
  213. foreach ($packages as $package) {
  214. if ($first && $package->getStatus() == FeaturesManagerInterface::STATUS_NO_EXPORT) {
  215. // Don't offer new non-profile packages that are empty.
  216. if ($package->getStatus() === FeaturesManagerInterface::STATUS_NO_EXPORT &&
  217. !$this->assigner->getBundle()->isProfilePackage($package->getMachineName()) &&
  218. empty($package->getConfig())) {
  219. continue;
  220. }
  221. $first = FALSE;
  222. $options[] = array(
  223. 'name' => array(
  224. 'data' => $this->t('The following packages are not exported.'),
  225. 'class' => 'features-export-header-row',
  226. 'colspan' => 6,
  227. ),
  228. );
  229. }
  230. $options[$package->getMachineName()] = $this->buildPackageDetail($package);
  231. }
  232. $element = array(
  233. '#type' => 'tableselect',
  234. '#header' => $header,
  235. '#options' => $options,
  236. '#attributes' => array('class' => array('features-listing')),
  237. '#prefix' => '<div id="edit-features-preview-wrapper">',
  238. '#suffix' => '</div>',
  239. );
  240. return $element;
  241. }
  242. /**
  243. * Builds the details of a package.
  244. *
  245. * @param \Drupal\features\Package $package
  246. * The package.
  247. *
  248. * @return array
  249. * A render array of a form element.
  250. */
  251. protected function buildPackageDetail(Package $package) {
  252. $config_collection = $this->featuresManager->getConfigCollection();
  253. $url = Url::fromRoute('features.edit', array('featurename' => $package->getMachineName()));
  254. $element['name'] = array(
  255. 'data' => \Drupal::l($package->getName(), $url),
  256. 'class' => array('feature-name'),
  257. );
  258. $machine_name = $package->getMachineName();
  259. // Except for the 'unpackaged' pseudo-package, display the full name, since
  260. // that's what will be generated.
  261. if ($machine_name !== 'unpackaged') {
  262. $machine_name = $package->getFullName($machine_name);
  263. }
  264. $element['machine_name'] = $machine_name;
  265. $element['status'] = array(
  266. 'data' => $this->featuresManager->statusLabel($package->getStatus()),
  267. 'class' => array('column-nowrap'),
  268. );
  269. // Use 'data' instead of plain string value so a blank version doesn't
  270. // remove column from table.
  271. $element['version'] = array(
  272. 'data' => Html::escape($package->getVersion()),
  273. 'class' => array('column-nowrap'),
  274. );
  275. $overrides = $this->featuresManager->detectOverrides($package);
  276. $new_config = $this->featuresManager->detectNew($package);
  277. $conflicts = array();
  278. $missing = array();
  279. if ($package->getStatus() == FeaturesManagerInterface::STATUS_NO_EXPORT) {
  280. $overrides = array();
  281. $new_config = array();
  282. }
  283. // Bundle package configuration by type.
  284. $package_config = array();
  285. foreach ($package->getConfig() as $item_name) {
  286. if (isset($config_collection[$item_name])) {
  287. $item = $config_collection[$item_name];
  288. $package_config[$item->getType()][] = array(
  289. 'name' => Html::escape($item_name),
  290. 'label' => Html::escape($item->getLabel()),
  291. 'class' => in_array($item_name, $overrides) ? 'features-override' :
  292. (in_array($item_name, $new_config) ? 'features-detected' : ''),
  293. );
  294. }
  295. }
  296. // Conflict config from other modules.
  297. foreach ($package->getConfigOrig() as $item_name) {
  298. if (!isset($config_collection[$item_name])) {
  299. $missing[] = $item_name;
  300. $package_config['missing'][] = array(
  301. 'name' => Html::escape($item_name),
  302. 'label' => Html::escape($item_name),
  303. 'class' => 'features-missing',
  304. );
  305. }
  306. elseif (!in_array($item_name, $package->getConfig())) {
  307. $item = $config_collection[$item_name];
  308. $conflicts[] = $item_name;
  309. $package_name = !empty($item->getPackage()) ? $item->getPackage() : $this->t('PACKAGE NOT ASSIGNED');
  310. $package_config[$item->getType()][] = array(
  311. 'name' => Html::escape($package_name),
  312. 'label' => Html::escape($item->getLabel()),
  313. 'class' => 'features-conflict',
  314. );
  315. }
  316. }
  317. // Add dependencies.
  318. $package_config['dependencies'] = array();
  319. foreach ($package->getDependencies() as $dependency) {
  320. $package_config['dependencies'][] = array(
  321. 'name' => $dependency,
  322. 'label' => $this->moduleHandler->getName($dependency),
  323. 'class' => '',
  324. );
  325. }
  326. $class = '';
  327. $state_links = [];
  328. if (!empty($conflicts)) {
  329. $state_links[] = array(
  330. '#type' => 'link',
  331. '#title' => $this->t('Conflicts'),
  332. '#url' => Url::fromRoute('features.edit', array('featurename' => $package->getMachineName())),
  333. '#attributes' => array('class' => array('features-conflict')),
  334. );
  335. }
  336. if (!empty($overrides)) {
  337. $state_links[] = array(
  338. '#type' => 'link',
  339. '#title' => $this->featuresManager->stateLabel(FeaturesManagerInterface::STATE_OVERRIDDEN),
  340. '#url' => Url::fromRoute('features.diff', array('featurename' => $package->getMachineName())),
  341. '#attributes' => array('class' => array('features-override')),
  342. );
  343. }
  344. if (!empty($new_config)) {
  345. $state_links[] = array(
  346. '#type' => 'link',
  347. '#title' => $this->t('New detected'),
  348. '#url' => Url::fromRoute('features.diff', array('featurename' => $package->getMachineName())),
  349. '#attributes' => array('class' => array('features-detected')),
  350. );
  351. }
  352. if (!empty($missing) && ($package->getStatus() == FeaturesManagerInterface::STATUS_INSTALLED)) {
  353. $state_links[] = array(
  354. '#type' => 'link',
  355. '#title' => $this->t('Missing'),
  356. '#url' => Url::fromRoute('features.edit', array('featurename' => $package->getMachineName())),
  357. '#attributes' => array('class' => array('features-missing')),
  358. );
  359. }
  360. if (!empty($state_links)) {
  361. $element['state'] = array(
  362. 'data' => $state_links,
  363. 'class' => array('column-nowrap'),
  364. );
  365. }
  366. else {
  367. $element['state'] = '';
  368. }
  369. $config_types = $this->featuresManager->listConfigTypes();
  370. // Add dependencies.
  371. $config_types['dependencies'] = $this->t('Dependencies');
  372. $config_types['missing'] = $this->t('Missing');
  373. uasort($config_types, 'strnatcasecmp');
  374. $rows = array();
  375. // Use sorted array for order.
  376. foreach ($config_types as $type => $label) {
  377. // For each component type, offer alternating rows.
  378. $row = array();
  379. if (isset($package_config[$type])) {
  380. $row[] = array(
  381. 'data' => array(
  382. '#type' => 'html_tag',
  383. '#tag' => 'span',
  384. '#value' => Html::escape($label),
  385. '#attributes' => array(
  386. 'title' => Html::escape($type),
  387. 'class' => 'features-item-label',
  388. ),
  389. ),
  390. );
  391. $row[] = array(
  392. 'data' => array(
  393. '#theme' => 'features_items',
  394. '#items' => $package_config[$type],
  395. '#value' => Html::escape($label),
  396. '#title' => Html::escape($type),
  397. ),
  398. 'class' => 'item',
  399. );
  400. $rows[] = $row;
  401. }
  402. }
  403. $element['table'] = array(
  404. '#type' => 'table',
  405. '#rows' => $rows,
  406. );
  407. $details = array();
  408. $details['description'] = array(
  409. '#markup' => Xss::filterAdmin($package->getDescription()),
  410. );
  411. $details['table'] = array(
  412. '#type' => 'details',
  413. '#title' => array('#markup' => $this->t('Included configuration')),
  414. '#description' => array('data' => $element['table']),
  415. );
  416. $element['details'] = array(
  417. 'class' => array('description', 'expand'),
  418. 'data' => $details,
  419. );
  420. return $element;
  421. }
  422. /**
  423. * Adds a pseudo-package to display unpackaged configuration.
  424. *
  425. * @param array $packages
  426. * An array of package names.
  427. * @param \Drupal\features\ConfigurationItem[] $config_collection
  428. * A collection of configuration.
  429. */
  430. protected function addUnpackaged(array &$packages, array $config_collection) {
  431. $packages['unpackaged'] = new Package('unpackaged', [
  432. 'name' => $this->t('Unpackaged'),
  433. 'description' => $this->t('Configuration that has not been added to any package.'),
  434. 'config' => [],
  435. 'status' => FeaturesManagerInterface::STATUS_NO_EXPORT,
  436. 'version' => '',
  437. ]);
  438. foreach ($config_collection as $item_name => $item) {
  439. if (!$item->getPackage() && !$item->isExcluded() && !$item->isProviderExcluded()) {
  440. $packages['unpackaged']->appendConfig($item_name);
  441. }
  442. }
  443. }
  444. /**
  445. * Denies access to the checkboxes for uninstalled or empty packages and the
  446. * "unpackaged" pseudo-package.
  447. *
  448. * @param array $form
  449. * The form build array to alter.
  450. *
  451. * @return array
  452. * The form build array.
  453. */
  454. public static function preRenderRemoveInvalidCheckboxes(array $form) {
  455. /** @var \Drupal\features\Package $package */
  456. foreach ($form['#packages'] as $package) {
  457. // Remove checkboxes for packages that:
  458. // - have no configuration assigned and are not the profile, or
  459. // - are the "unpackaged" pseudo-package.
  460. if ((empty($package->getConfig()) && !($package->getMachineName() == $form['#profile_package'])) ||
  461. $package->getMachineName() == 'unpackaged') {
  462. $form['preview'][$package->getMachineName()]['#access'] = FALSE;
  463. }
  464. }
  465. return $form;
  466. }
  467. /**
  468. * {@inheritdoc}
  469. */
  470. public function submitForm(array &$form, FormStateInterface $form_state) {
  471. $current_bundle = $this->assigner->loadBundle();
  472. $this->assigner->assignConfigPackages();
  473. $package_names = array_filter($form_state->getValue('preview'));
  474. if (empty($package_names)) {
  475. drupal_set_message($this->t('Please select one or more packages to export.'), 'warning');
  476. return;
  477. }
  478. $method_id = NULL;
  479. $trigger = $form_state->getTriggeringElement();
  480. $op = $form_state->getValue('op');
  481. if (!empty($trigger) && empty($op)) {
  482. $method_id = $trigger['#name'];
  483. }
  484. if (!empty($method_id)) {
  485. $this->generator->generatePackages($method_id, $current_bundle, $package_names);
  486. $this->generator->applyExportFormSubmit($method_id, $form, $form_state);
  487. }
  488. }
  489. }