FeaturesEditForm.php 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  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\features\ConfigurationItem;
  9. use Drupal\Core\Form\FormBase;
  10. use Drupal\Core\Form\FormStateInterface;
  11. use Symfony\Component\DependencyInjection\ContainerInterface;
  12. Use Drupal\Component\Render\FormattableMarkup;
  13. use Drupal\config_update\ConfigRevertInterface;
  14. /**
  15. * Defines the features settings form.
  16. */
  17. class FeaturesEditForm extends FormBase {
  18. /**
  19. * The features manager.
  20. *
  21. * @var array
  22. */
  23. protected $featuresManager;
  24. /**
  25. * The package assigner.
  26. *
  27. * @var array
  28. */
  29. protected $assigner;
  30. /**
  31. * The package generator.
  32. *
  33. * @var array
  34. */
  35. protected $generator;
  36. /**
  37. * Current package being edited.
  38. *
  39. * @var \Drupal\features\Package
  40. */
  41. protected $package;
  42. /**
  43. * Current bundle machine name.
  44. *
  45. * NOTE: D8 cannot serialize objects within forms so you can't directly
  46. * store the entire Bundle object here.
  47. *
  48. * @var string
  49. */
  50. protected $bundle;
  51. /**
  52. * Previous bundle name for ajax processing.
  53. *
  54. * @var string
  55. */
  56. protected $oldBundle;
  57. /**
  58. * Config to be specifically excluded.
  59. *
  60. * @var array
  61. */
  62. protected $excluded;
  63. /**
  64. * Config to be specifically required.
  65. *
  66. * @var array
  67. */
  68. protected $required;
  69. /**
  70. * Config referenced by other packages.
  71. *
  72. * @var array
  73. */
  74. protected $conflicts;
  75. /**
  76. * Determine if conflicts are allowed to be added.
  77. *
  78. * @var bool
  79. */
  80. protected $allowConflicts;
  81. /**
  82. * Config missing from active site.
  83. *
  84. * @var array
  85. */
  86. protected $missing;
  87. /**
  88. * The config reverter.
  89. *
  90. * @var \Drupal\config_update\ConfigRevertInterface
  91. */
  92. protected $configRevert;
  93. /**
  94. * Constructs a FeaturesEditForm object.
  95. *
  96. * @param \Drupal\features\FeaturesManagerInterface $features_manager
  97. * The features manager.
  98. */
  99. public function __construct(FeaturesManagerInterface $features_manager, FeaturesAssignerInterface $assigner, FeaturesGeneratorInterface $generator, ConfigRevertInterface $config_revert) {
  100. $this->featuresManager = $features_manager;
  101. $this->assigner = $assigner;
  102. $this->generator = $generator;
  103. $this->configRevert = $config_revert;
  104. $this->excluded = [];
  105. $this->required = [];
  106. $this->conflicts = [];
  107. $this->missing = [];
  108. }
  109. /**
  110. * {@inheritdoc}
  111. */
  112. public static function create(ContainerInterface $container) {
  113. return new static(
  114. $container->get('features.manager'),
  115. $container->get('features_assigner'),
  116. $container->get('features_generator'),
  117. $container->get('features.config_update')
  118. );
  119. }
  120. /**
  121. * {@inheritdoc}
  122. */
  123. public function getFormId() {
  124. return 'features_edit_form';
  125. }
  126. /**
  127. * {@inheritdoc}
  128. */
  129. public function buildForm(array $form, FormStateInterface $form_state, $featurename = '') {
  130. $session = $this->getRequest()->getSession();
  131. $trigger = $form_state->getTriggeringElement();
  132. if ($trigger['#name'] == 'package') {
  133. // Save current bundle name for later ajax callback.
  134. $this->oldBundle = $this->bundle;
  135. }
  136. elseif ($trigger['#name'] == 'conflicts') {
  137. if (isset($session)) {
  138. $session->set('features_allow_conflicts', $form_state->getValue('conflicts'));
  139. }
  140. }
  141. if (!$form_state->isValueEmpty('package')) {
  142. $bundle_name = $form_state->getValue('package');
  143. $bundle = $this->assigner->getBundle($bundle_name);
  144. }
  145. else {
  146. $bundle = $this->assigner->loadBundle();
  147. }
  148. // Only store bundle name, not full object.
  149. $this->bundle = $bundle->getMachineName();
  150. $this->allowConflicts = FALSE;
  151. if (isset($session)) {
  152. $this->allowConflicts = $session->get('features_allow_conflicts', FALSE);
  153. }
  154. // Pass the $force argument as TRUE because we want to include any excluded
  155. // configuration items. These should show up as automatically assigned, but
  156. // not selected, thus allowing the admin to reselect if desired.
  157. // @see FeaturesManagerInterface::assignConfigPackage()
  158. $this->assigner->assignConfigPackages(TRUE);
  159. $packages = $this->featuresManager->getPackages();
  160. if (empty($packages[$featurename])) {
  161. $featurename = str_replace(array('-', ' '), '_', $featurename);
  162. $this->package = $this->featuresManager->initPackage($featurename, NULL, '', 'module', $bundle);
  163. }
  164. else {
  165. $this->package = $packages[$featurename];
  166. }
  167. if (!empty($packages[$featurename]) && $this->package->getBundle() !== $this->bundle && $form_state->isValueEmpty('package')) {
  168. // Make sure the current bundle matches what is stored in the package.
  169. // But only do this if the Package value hasn't been manually changed.
  170. $bundle = $this->assigner->getBundle($this->package->getBundle());
  171. $this->bundle = $bundle->getMachineName();
  172. $this->assigner->reset();
  173. $this->assigner->assignConfigPackages(TRUE);
  174. $packages = $this->featuresManager->getPackages();
  175. $this->package = $packages[$featurename];
  176. }
  177. $form = array(
  178. '#show_operations' => FALSE,
  179. '#prefix' => '<div id="features-edit-wrapper">',
  180. '#suffix' => '</div>',
  181. );
  182. $form['info'] = array(
  183. '#type' => 'fieldset',
  184. '#title' => $this->t('General Information'),
  185. '#tree' => FALSE,
  186. '#weight' => 2,
  187. '#prefix' => "<div id='features-export-info'>",
  188. '#suffix' => '</div>',
  189. );
  190. $form['info']['name'] = array(
  191. '#title' => $this->t('Name'),
  192. '#description' => $this->t('Example: Image gallery') . ' (' . $this->t('Do not begin name with numbers.') . ')',
  193. '#type' => 'textfield',
  194. '#default_value' => $this->package->getName(),
  195. );
  196. if (!$bundle->isDefault()) {
  197. $form['info']['name']['#description'] .= '<br/>' .
  198. $this->t('The namespace "@name_" will be prepended to the machine name', array('@name' => $bundle->getMachineName()));
  199. }
  200. $form['info']['machine_name'] = array(
  201. '#type' => 'machine_name',
  202. '#title' => $this->t('Machine-readable name'),
  203. '#description' => $this->t('Example: image_gallery') . ' ' . $this->t('May only contain lowercase letters, numbers and underscores.'),
  204. '#required' => TRUE,
  205. '#default_value' => $bundle->getShortName($this->package->getMachineName()),
  206. '#machine_name' => array(
  207. 'source' => array('info', 'name'),
  208. 'exists' => array($this, 'featureExists'),
  209. ),
  210. );
  211. if (!$bundle->isDefault()) {
  212. $form['info']['machine_name']['#description'] .= '<br/>' .
  213. $this->t('NOTE: Do NOT include the namespace prefix "@name_"; it will be added automatically.', array('@name' => $bundle->getMachineName()));
  214. }
  215. $form['info']['description'] = array(
  216. '#title' => $this->t('Description'),
  217. '#description' => $this->t('Provide a short description of what users should expect when they install your feature.'),
  218. '#type' => 'textarea',
  219. '#rows' => 3,
  220. '#default_value' => $this->package->getDescription(),
  221. );
  222. $form['info']['package'] = array(
  223. '#title' => $this->t('Bundle'),
  224. '#type' => 'select',
  225. '#options' => $this->assigner->getBundleOptions(),
  226. '#default_value' => $bundle->getMachineName(),
  227. '#ajax' => array(
  228. 'callback' => '::updateBundle',
  229. 'wrapper' => 'features-export-info',
  230. ),
  231. );
  232. $form['info']['version'] = array(
  233. '#title' => $this->t('Version'),
  234. '#description' => $this->t('Examples: 8.x-1.0, 8.x-1.0-beta1'),
  235. '#type' => 'textfield',
  236. '#required' => FALSE,
  237. '#default_value' => $this->package->getVersion(),
  238. '#size' => 30,
  239. );
  240. list($full_name, $path) = $this->featuresManager->getExportInfo($this->package, $bundle);
  241. $form['info']['directory'] = array(
  242. '#title' => $this->t('Path'),
  243. '#description' => $this->t('Path to export package using Write action, relative to root directory.'),
  244. '#type' => 'textfield',
  245. '#required' => FALSE,
  246. '#default_value' => $path,
  247. '#size' => 30,
  248. );
  249. $require_all = $this->package->getRequiredAll();
  250. $form['info']['require_all'] = array(
  251. '#type' => 'checkbox',
  252. '#title' => $this->t('Mark all config as required'),
  253. '#default_value' => $this->package->getRequiredAll(),
  254. '#description' => $this->t('Required config will be assigned to this feature regardless of other assignment plugins.'),
  255. );
  256. $form['conflicts'] = array(
  257. '#type' => 'checkbox',
  258. '#title' => $this->t('Allow conflicts'),
  259. '#default_value' => $this->allowConflicts,
  260. '#description' => $this->t('Allow configuration to be exported to more than one feature.'),
  261. '#weight' => 8,
  262. '#ajax' => array(
  263. 'callback' => '::updateForm',
  264. 'wrapper' => 'features-edit-wrapper',
  265. ),
  266. );
  267. $generation_info = array();
  268. if (\Drupal::currentUser()->hasPermission('export configuration')) {
  269. // Offer available generation methods.
  270. $generation_info = $this->generator->getGenerationMethods();
  271. // Sort generation methods by weight.
  272. uasort($generation_info, '\Drupal\Component\Utility\SortArray::sortByWeightElement');
  273. }
  274. $form['actions'] = array('#type' => 'actions', '#tree' => TRUE);
  275. foreach ($generation_info as $method_id => $method) {
  276. $form['actions'][$method_id] = array(
  277. '#type' => 'submit',
  278. '#name' => $method_id,
  279. '#value' => $this->t('@name', array('@name' => $method['name'])),
  280. '#attributes' => array(
  281. 'title' => Html::escape($method['description']),
  282. ),
  283. );
  284. }
  285. // Build the Component Listing panel on the right.
  286. $form['export'] = $this->buildComponentList($form_state);
  287. if (!empty($this->missing)) {
  288. if ($this->allowConflicts) {
  289. $form['actions']['#prefix'] = '<strong>' .
  290. $this->t('WARNING: Package contains configuration missing from site.') . '<br>' .
  291. $this->t('This configuration will be removed if you export it.') .
  292. '</strong>';
  293. }
  294. else {
  295. foreach ($generation_info as $method_id => $method) {
  296. unset($form['actions'][$method_id]);
  297. }
  298. $form['actions']['#prefix'] = '<strong>' .
  299. $this->t('Package contains configuration missing from site.') . '<br>' .
  300. $this->t('Import the feature to create the missing config before you can export it.') . '<br>' .
  301. $this->t('Or, enable the Allow Conflicts option above.') .
  302. '</strong>';
  303. }
  304. $form['actions']['import_missing'] = array(
  305. '#type' => 'submit',
  306. '#name' => 'import_missing',
  307. '#value' => $this->t('Import Missing'),
  308. '#attributes' => array(
  309. 'title' => $this->t('Import only the missing configuration items.'),
  310. ),
  311. );
  312. }
  313. $form['#attached'] = array(
  314. 'library' => array(
  315. 'features_ui/drupal.features_ui.admin',
  316. ),
  317. 'drupalSettings' => array(
  318. 'features' => array(
  319. 'excluded' => $this->excluded,
  320. 'required' => $this->required,
  321. 'conflicts' => $this->conflicts,
  322. 'autodetect' => TRUE,
  323. ),
  324. ),
  325. );
  326. return $form;
  327. }
  328. /**
  329. * Provides an ajax callback for handling conflict checkbox.
  330. */
  331. public function updateForm($form, FormStateInterface $form_state) {
  332. return $form;
  333. }
  334. /**
  335. * Provides an ajax callback for handling switching the bundle selector.
  336. */
  337. public function updateBundle($form, FormStateInterface $form_state) {
  338. $old_bundle = $this->assigner->getBundle($this->oldBundle);
  339. $bundle_name = $form_state->getValue('package');
  340. $bundle = $this->assigner->getBundle($bundle_name);
  341. if (isset($bundle) && isset($old_bundle)) {
  342. $short_name = $old_bundle->getShortName($this->package->getMachineName());
  343. if ($bundle->isDefault()) {
  344. $short_name = $old_bundle->getFullName($short_name);
  345. }
  346. $this->package->setMachineName($bundle->getFullName($short_name));
  347. $form['info']['machine_name']['#value'] = $bundle->getShortName($this->package->getMachineName());
  348. }
  349. return $form['info'];
  350. }
  351. /**
  352. * Callback for machine_name exists()
  353. * @param $value
  354. * @param $element
  355. * @param $form_state
  356. * @return bool
  357. */
  358. public function featureExists($value, $element, $form_state) {
  359. $packages = $this->featuresManager->getPackages();
  360. return isset($packages[$value]) || \Drupal::moduleHandler()->moduleExists($value);
  361. }
  362. /**
  363. * Returns the render array elements for the Components selection on the Edit
  364. * form.
  365. */
  366. protected function buildComponentList(FormStateInterface $form_state) {
  367. $element = array(
  368. '#type' => 'fieldset',
  369. '#title' => $this->t('Components'),
  370. '#description' => $this->t('Expand each component section and select which items should be included in this feature export.'),
  371. '#tree' => FALSE,
  372. '#prefix' => "<div id='features-export-wrapper'>",
  373. '#suffix' => '</div>',
  374. '#weight' => 1,
  375. );
  376. // Filter field used in javascript, so javascript will unhide it.
  377. $element['features_filter_wrapper'] = array(
  378. '#type' => 'fieldset',
  379. '#title' => $this->t('Filters'),
  380. '#tree' => FALSE,
  381. '#prefix' => "<div id='features-filter' class='element-invisible'>",
  382. '#suffix' => '</div>',
  383. '#weight' => -10,
  384. );
  385. $element['features_filter_wrapper']['features_filter'] = array(
  386. '#type' => 'textfield',
  387. '#title' => $this->t('Search'),
  388. '#hidden' => TRUE,
  389. '#default_value' => '',
  390. '#suffix' => "<span class='features-filter-clear'>" . $this->t('Clear') . "</span>",
  391. );
  392. $element['features_filter_wrapper']['checkall'] = array(
  393. '#type' => 'checkbox',
  394. '#default_value' => FALSE,
  395. '#hidden' => TRUE,
  396. '#title' => $this->t('Select all'),
  397. '#attributes' => array(
  398. 'class' => array('features-checkall'),
  399. ),
  400. );
  401. $sections = array('included', 'detected', 'added');
  402. $config_types = $this->featuresManager->listConfigTypes();
  403. // Generate the export array for the current feature and user selections.
  404. $export = $this->getComponentList($form_state);
  405. foreach ($export['components'] as $component => $component_info) {
  406. $component_items_count = count($component_info['_features_options']['sources']);
  407. $label = new FormattableMarkup('@component (<span class="component-count">@count</span>)',
  408. array(
  409. '@component' => $config_types[$component],
  410. '@count' => $component_items_count,
  411. )
  412. );
  413. $count = 0;
  414. foreach ($sections as $section) {
  415. $count += count($component_info['_features_options'][$section]);
  416. }
  417. $extra_class = ($count == 0) ? 'features-export-empty' : '';
  418. $component_name = str_replace('_', '-', Html::escape($component));
  419. if ($count + $component_items_count > 0) {
  420. $element[$component] = array(
  421. '#markup' => '',
  422. '#tree' => TRUE,
  423. );
  424. $element[$component]['sources'] = array(
  425. '#type' => 'details',
  426. '#title' => $label,
  427. '#tree' => TRUE,
  428. '#open' => FALSE,
  429. '#attributes' => array('class' => array('features-export-component')),
  430. '#prefix' => "<div class='features-export-parent component-$component'>",
  431. );
  432. $element[$component]['sources']['selected'] = array(
  433. '#type' => 'checkboxes',
  434. '#id' => "edit-sources-$component_name",
  435. '#options' => $this->domDecodeOptions($component_info['_features_options']['sources']),
  436. '#default_value' => $this->domDecodeOptions($component_info['_features_selected']['sources'], FALSE),
  437. '#attributes' => array('class' => array('component-select')),
  438. '#prefix' => "<span class='component-select'>",
  439. '#suffix' => '</span>',
  440. );
  441. $element[$component]['before-list'] = array(
  442. '#markup' => "<div class='component-list features-export-list $extra_class'>",
  443. );
  444. foreach ($sections as $section) {
  445. $element[$component][$section] = array(
  446. '#type' => 'checkboxes',
  447. '#options' => !empty($component_info['_features_options'][$section]) ?
  448. $this->domDecodeOptions($component_info['_features_options'][$section]) : array(),
  449. '#default_value' => !empty($component_info['_features_selected'][$section]) ?
  450. $this->domDecodeOptions($component_info['_features_selected'][$section], FALSE) : array(),
  451. '#attributes' => array('class' => array('component-' . $section)),
  452. '#prefix' => "<span class='component-$section'>",
  453. '#suffix' => '</span>',
  454. );
  455. }
  456. // Close both the before-list as well as the sources div.
  457. $element[$component]['after-list'] = array(
  458. '#markup' => "</div></div>",
  459. );
  460. }
  461. }
  462. $element['features_missing'] = array(
  463. '#theme' => 'item_list',
  464. '#items' => $export['missing'],
  465. '#title' => $this->t('Configuration missing from active site:'),
  466. '#suffix' => '<div class="description">' .
  467. $this->t('Import the feature to create the missing config listed above.') .
  468. '</div>',
  469. );
  470. $element['features_legend'] = array(
  471. '#type' => 'fieldset',
  472. '#title' => $this->t('Legend'),
  473. '#tree' => FALSE,
  474. '#prefix' => "<div id='features-legend'>",
  475. '#suffix' => '</div>',
  476. );
  477. $element['features_legend']['legend'] = array(
  478. '#markup' =>
  479. "<span class='component-included'>" . $this->t('Normal') . "</span> " .
  480. "<span class='component-added'>" . $this->t('Added') . "</span> " .
  481. "<span class='component-detected'>" . $this->t('Auto detected') . "</span> " .
  482. "<span class='component-conflict'>" . $this->t('Conflict') . "</span> ",
  483. );
  484. return $element;
  485. }
  486. /**
  487. * Returns the full feature export array based upon user selections in
  488. * form_state.
  489. *
  490. * @param \Drupal\Core\Form\FormStateInterface $form_state
  491. * Optional form_state information for user selections. Can be updated to
  492. * reflect new selection status.
  493. *
  494. * @return \Drupal\features\Package
  495. * New export array to be exported
  496. * array['components'][$component_name] = $component_info
  497. * $component_info['_features_options'][$section] is list of available options
  498. * $component_info['_features_selected'][$section] is option state TRUE/FALSE
  499. * $section = array('sources', included', 'detected', 'added')
  500. * sources - options that are available to be added to the feature
  501. * included - options that have been previously exported to the feature
  502. * detected - options that have been auto-detected
  503. * added - newly added options to the feature
  504. *
  505. * NOTE: This routine gets a bit complex to handle all of the different
  506. * possible user checkbox selections and de-selections.
  507. * Cases to test:
  508. * 1a) uncheck Included item -> mark as Added but unchecked
  509. * 1b) re-check unchecked Added item -> return it to Included check item
  510. * 2a) check Sources item -> mark as Added and checked
  511. * 2b) uncheck Added item -> return it to Sources as unchecked
  512. * 3a) uncheck Included item that still exists as auto-detect -> mark as
  513. * Detected but unchecked
  514. * 3b) re-check Detected item -> return it to Included and checked
  515. * 4a) check Sources item should also add any auto-detect items as Detected
  516. * and checked
  517. * 4b) uncheck Sources item with auto-detect and auto-detect items should
  518. * return to Sources and unchecked
  519. * 5a) uncheck a Detected item -> refreshing page should keep it as
  520. * unchecked Detected
  521. * 6) when nothing changes, refresh should not change any state
  522. * 7) should never see an unchecked Included item
  523. */
  524. protected function getComponentList(FormStateInterface $form_state) {
  525. $config = $this->featuresManager->getConfigCollection();
  526. $package_name = $this->package->getMachineName();
  527. // Auto-detect dependencies for included config.
  528. $package_config = $this->package->getConfig();
  529. if (!empty($this->package->getConfigOrig())) {
  530. $package_config = array_unique(array_merge($package_config, $this->package->getConfigOrig()));
  531. }
  532. if (!empty($package_config)) {
  533. $this->featuresManager->assignConfigDependents($package_config, $package_name);
  534. }
  535. $packages = $this->featuresManager->getPackages();
  536. // Re-fetch the package in case config was updated with Dependents above.
  537. $this->package = $packages[$package_name];
  538. // Make a map of all config data.
  539. $components = array();
  540. $this->conflicts = array();
  541. foreach ($config as $item_name => $item) {
  542. if (($item->getPackage() != $package_name) &&
  543. !empty($packages[$item->getPackage()]) && ($packages[$item->getPackage()]->getStatus() != FeaturesManagerInterface::STATUS_NO_EXPORT)) {
  544. $this->conflicts[$item->getType()][$item->getShortName()] = $item->getLabel();
  545. }
  546. if ($this->allowConflicts
  547. || !isset($this->conflicts[$item->getType()][$item->getShortName()])
  548. || ($this->package->getConfigOrig() && in_array($item_name, $this->package->getConfigOrig()))) {
  549. $components[$item->getType()][$item->getShortName()] = $item->getLabel();
  550. }
  551. }
  552. // Make a map of the config data already exported to the Feature.
  553. $this->missing = array();
  554. $exported_features_info = array();
  555. foreach ($this->package->getConfigOrig() as $item_name) {
  556. // Make sure the extension provided item exists in the active
  557. // configuration storage.
  558. if (isset($config[$item_name])) {
  559. $item = $config[$item_name];
  560. // Remove any conflicts if those are not being allowed.
  561. // if ($this->allowConflicts || !isset($this->conflicts[$item['type']][$item['name_short']])) {
  562. $exported_features_info[$item->getType()][$item->getShortName()] = $item->getLabel();
  563. // }
  564. }
  565. else {
  566. $this->missing[] = $item_name;
  567. }
  568. }
  569. $exported_features_info['dependencies'] = $this->package->getDependencyInfo();
  570. // Make a map of any config specifically excluded and/or required.
  571. foreach (array('excluded', 'required') as $constraint) {
  572. $this->{$constraint} = array();
  573. $info = !empty($this->package->{'get' . $constraint}()) ? $this->package->{'get' . $constraint}() : array();
  574. if (($constraint == 'required') && (empty($info) || !is_array($info))) {
  575. // If required is True or empty array, add all config as required
  576. $info = $this->package->getConfigOrig();
  577. }
  578. foreach ($info as $item_name) {
  579. if (!isset($config[$item_name])) {
  580. continue;
  581. }
  582. $item = $config[$item_name];
  583. $this->{$constraint}[$item->getType()][$item->getShortName()] = $item->getLabel();
  584. }
  585. }
  586. // Make a map of the config data to be exported within the Feature.
  587. $new_features_info = array();
  588. foreach ($this->package->getConfig() as $item_name) {
  589. $item = $config[$item_name];
  590. $new_features_info[$item->getType()][$item->getShortName()] = $item->getLabel();
  591. }
  592. $new_features_info['dependencies'] = $this->package->getDependencies();
  593. // Assemble the combined component list.
  594. $config_new = array();
  595. $sections = array('sources', 'included', 'detected', 'added');
  596. // Generate list of config to be exported.
  597. $config_count = array();
  598. foreach ($components as $component => $component_info) {
  599. // User-selected components take precedence.
  600. $config_new[$component] = array();
  601. $config_count[$component] = 0;
  602. // Add selected items from Sources checkboxes.
  603. if (!$form_state->isValueEmpty(array($component, 'sources', 'selected'))) {
  604. $config_new[$component] = array_merge($config_new[$component], $this->domDecodeOptions(array_filter($form_state->getValue(array(
  605. $component,
  606. 'sources',
  607. 'selected',
  608. )))));
  609. $config_count[$component]++;
  610. }
  611. // Add selected items from already Included, newly Added, auto-detected
  612. // checkboxes.
  613. foreach (array('included', 'added', 'detected') as $section) {
  614. if (!$form_state->isValueEmpty(array($component, $section))) {
  615. $config_new[$component] = array_merge($config_new[$component], $this->domDecodeOptions(array_filter($form_state->getValue(array($component, $section)))));
  616. $config_count[$component]++;
  617. }
  618. }
  619. // Only fallback to an existing feature's values if there are no export
  620. // options for the component.
  621. if ($component == 'dependencies') {
  622. if (($config_count[$component] == 0) && !empty($exported_features_info['dependencies'])) {
  623. $config_new[$component] = array_combine($exported_features_info['dependencies'], $exported_features_info['dependencies']);
  624. }
  625. }
  626. elseif (($config_count[$component] == 0) && !empty($exported_features_info[$component])) {
  627. $config_names = array_keys($exported_features_info[$component]);
  628. $config_new[$component] = array_combine($config_names, $config_names);
  629. }
  630. }
  631. // Generate new populated feature.
  632. $export['package'] = $this->package;
  633. $export['config_new'] = $config_new;
  634. // Now fill the $export with categorized sections of component options
  635. // based upon user selections and de-selections.
  636. foreach ($components as $component => $component_info) {
  637. $component_export = $component_info;
  638. foreach ($sections as $section) {
  639. $component_export['_features_options'][$section] = array();
  640. $component_export['_features_selected'][$section] = array();
  641. }
  642. if (!empty($component_info)) {
  643. $exported_components = !empty($exported_features_info[$component]) ? $exported_features_info[$component] : array();
  644. $new_components = !empty($new_features_info[$component]) ? $new_features_info[$component] : array();
  645. foreach ($component_info as $key => $label) {
  646. $config_name = $this->featuresManager->getFullName($component, $key);
  647. // If checkbox in Sources is checked, move it to Added section.
  648. if (!$form_state->isValueEmpty(array($component, 'sources', 'selected', $key))) {
  649. $form_state->setValue(array($component, 'sources', 'selected', $key), FALSE);
  650. $form_state->setValue(array($component, 'added', $key), 1);
  651. $component_export['_features_options']['added'][$key] = $this->configLabel($component, $key, $label);
  652. $component_export['_features_selected']['added'][$key] = $key;
  653. // If this was previously excluded, we don't need to set it as
  654. // required because it was automatically assigned.
  655. if (isset($this->excluded[$component][$key])) {
  656. unset($this->excluded[$component][$key]);
  657. }
  658. else {
  659. $this->required[$component][$key] = $key;
  660. }
  661. }
  662. elseif (isset($new_components[$key]) || isset($config_new[$component][$key])) {
  663. // Option is in the New exported array.
  664. if (isset($exported_components[$key])) {
  665. // Option was already previously exported so it's part of the
  666. // Included checkboxes.
  667. $section = 'included';
  668. $default_value = $key;
  669. // If Included item was un-selected (removed from export
  670. // $config_new) but was re-detected in the $new_components
  671. // means it was an auto-detect that was previously part of the
  672. // export and is now de-selected in UI.
  673. if ($form_state->isSubmitted() &&
  674. ($form_state->hasValue(array($component, 'included', $key)) ||
  675. ($form_state->isValueEmpty(array($component, 'detected', $key)))) &&
  676. empty($config_new[$component][$key])) {
  677. $section = 'detected';
  678. $default_value = FALSE;
  679. }
  680. // Unless it's unchecked in the form, then move it to Newly
  681. // disabled item.
  682. elseif ($form_state->isSubmitted() &&
  683. $form_state->isValueEmpty(array($component, 'added', $key)) &&
  684. $form_state->isValueEmpty(array($component, 'detected', $key)) &&
  685. $form_state->isValueEmpty(array($component, 'included', $key))) {
  686. $section = 'added';
  687. $default_value = FALSE;
  688. }
  689. }
  690. else {
  691. // Option was in New exported array, but NOT in already exported
  692. // so it's a user-selected or an auto-detect item.
  693. $section = 'detected';
  694. $default_value = NULL;
  695. // Check for item explicitly excluded.
  696. if (isset($this->excluded[$component][$key]) && !$form_state->isSubmitted()) {
  697. $default_value = FALSE;
  698. }
  699. else {
  700. $default_value = $key;
  701. }
  702. // If it's already checked in Added or Sources, leave it in Added
  703. // as checked.
  704. if ($form_state->isSubmitted() &&
  705. (!$form_state->isValueEmpty(array($component, 'added', $key)) ||
  706. !$form_state->isValueEmpty(array($component, 'sources', 'selected', $key)))) {
  707. $section = 'added';
  708. $default_value = $key;
  709. }
  710. // If it's already been unchecked, leave it unchecked.
  711. elseif ($form_state->isSubmitted() &&
  712. $form_state->isValueEmpty(array($component, 'sources', 'selected', $key)) &&
  713. $form_state->isValueEmpty(array($component, 'detected', $key)) &&
  714. !$form_state->hasValue(array($component, 'added', $key))) {
  715. $section = 'detected';
  716. $default_value = FALSE;
  717. }
  718. }
  719. $component_export['_features_options'][$section][$key] = $this->configLabel($component, $key, $label);
  720. $component_export['_features_selected'][$section][$key] = $default_value;
  721. // Save which dependencies are specifically excluded from
  722. // auto-detection.
  723. if (($section == 'detected') && ($default_value === FALSE)) {
  724. // If this was previously required, we don't need to set it as
  725. // excluded because it wasn't automatically assigned.
  726. if (!isset($this->required[$component][$key]) || ($this->package->getRequired() === TRUE)) {
  727. $this->excluded[$component][$key] = $key;
  728. }
  729. unset($this->required[$component][$key]);
  730. // Remove excluded item from export.
  731. if ($component == 'dependencies') {
  732. $export['package']->removeDependency($key);
  733. }
  734. else {
  735. $export['package']->removeConfig($config_name);
  736. }
  737. }
  738. else {
  739. unset($this->excluded[$component][$key]);
  740. }
  741. // Remove the 'input' and set the 'values' so Drupal stops looking
  742. // at 'input'.
  743. if ($form_state->isSubmitted()) {
  744. if (!$default_value) {
  745. $form_state->setValue(array($component, $section, $key), FALSE);
  746. }
  747. else {
  748. $form_state->setValue(array($component, $section, $key), 1);
  749. }
  750. }
  751. }
  752. elseif (!$form_state->isSubmitted() && isset($exported_components[$key])) {
  753. // Component is not part of new export, but was in original export.
  754. // Mark component as Added when creating initial form.
  755. $component_export['_features_options']['added'][$key] = $this->configLabel($component, $key, $label);
  756. $component_export['_features_selected']['added'][$key] = $key;
  757. }
  758. else {
  759. // Option was not part of the new export.
  760. $added = FALSE;
  761. foreach (array('included', 'added') as $section) {
  762. // Restore any user-selected checkboxes.
  763. if (!$form_state->isValueEmpty(array($component, $section, $key))) {
  764. $component_export['_features_options'][$section][$key] = $this->configLabel($component, $key, $label);
  765. $component_export['_features_selected'][$section][$key] = $key;
  766. $added = TRUE;
  767. }
  768. }
  769. if (!$added) {
  770. // If not Included or Added, then put it back in the unchecked
  771. // Sources checkboxes.
  772. $component_export['_features_options']['sources'][$key] = $this->configLabel($component, $key, $label);
  773. $component_export['_features_selected']['sources'][$key] = FALSE;
  774. }
  775. }
  776. }
  777. }
  778. $export['components'][$component] = $component_export;
  779. }
  780. $export['features_exclude'] = $this->excluded;
  781. $export['features_require'] = $this->required;
  782. $export['conflicts'] = $this->conflicts;
  783. $export['missing'] = $this->missing;
  784. return $export;
  785. }
  786. /**
  787. * Returns a formatted and sanitized label for a config item.
  788. *
  789. * @param string $type
  790. * The config type.
  791. * @param string $key
  792. * The short machine name of the item.
  793. * @param string $label
  794. * The human label for the item.
  795. */
  796. protected function configLabel($type, $key, $label) {
  797. $value = Html::escape($label);
  798. if ($key != $label) {
  799. $value .= ' <span class="config-name">(' . Html::escape($key) . ')</span>';
  800. }
  801. if (isset($this->conflicts[$type][$key])) {
  802. // Show what package the conflict is stored in.
  803. $config = $this->featuresManager->getConfigCollection();
  804. $config_name = $this->featuresManager->getFullName($type, $key);
  805. $package_name = isset($config[$config_name]) ? $config[$config_name]->getPackage() : '';
  806. // Get the full machine name instead of the short name.
  807. $packages = $this->featuresManager->getPackages();
  808. if (isset($packages[$package_name])) {
  809. $package_name = $packages[$package_name]->getMachineName();
  810. }
  811. $value .= ' <span class="config-name">[' . $this->t('in') . ' ' . Html::escape($package_name) . ']</span>';
  812. }
  813. return Xss::filterAdmin($value);
  814. }
  815. /**
  816. * {@inheritdoc}
  817. */
  818. public function submitForm(array &$form, FormStateInterface $form_state) {
  819. $bundle = $this->assigner->getBundle($this->bundle);
  820. $this->assigner->assignConfigPackages();
  821. $this->package->setName($form_state->getValue('name'));
  822. $this->package->setMachineName($form_state->getValue('machine_name'));
  823. $this->package->setDescription($form_state->getValue('description'));
  824. $this->package->setVersion($form_state->getValue('version'));
  825. $this->package->setDirectory($form_state->getValue('directory'));
  826. $this->package->setBundle($bundle->getMachineName());
  827. // Save it first just to create it in case it's a new package.
  828. $this->featuresManager->setPackage($this->package);
  829. $config = $this->updatePackageConfig($form_state);
  830. $this->featuresManager->assignConfigPackage($this->package->getMachineName(), $config, TRUE);
  831. $this->package->setExcluded($this->updateExcluded());
  832. if ($form_state->getValue('require_all')) {
  833. $this->package->setRequired(TRUE);
  834. }
  835. else {
  836. $required = $this->updateRequired();
  837. $this->package->setRequired($required);
  838. }
  839. // Now save it with the selected config data.
  840. $this->featuresManager->setPackage($this->package);
  841. $method_id = NULL;
  842. $trigger = $form_state->getTriggeringElement();
  843. $op = $form_state->getValue('op');
  844. if (!empty($trigger) && empty($op)) {
  845. $method_id = $trigger['#name'];
  846. }
  847. // Set default redirect, but allow generators to change it later.
  848. $form_state->setRedirect('features.edit', array('featurename' => $this->package->getMachineName()));
  849. if ($method_id == 'import_missing') {
  850. $this->importMissing();
  851. }
  852. elseif (!empty($method_id)) {
  853. $packages = array($this->package->getMachineName());
  854. $this->generator->generatePackages($method_id, $bundle, $packages);
  855. $this->generator->applyExportFormSubmit($method_id, $form, $form_state);
  856. }
  857. $this->assigner->setCurrent($bundle);
  858. }
  859. /**
  860. * Updates the config stored in the package from the current edit form.
  861. *
  862. * @return array
  863. * Config array to be exported.
  864. */
  865. protected function updatePackageConfig(FormStateInterface $form_state) {
  866. $config = array();
  867. $components = $this->getComponentList($form_state);
  868. foreach ($components['config_new'] as $config_type => $items) {
  869. foreach ($items as $name) {
  870. $config[] = $this->featuresManager->getFullName($config_type, $name);
  871. }
  872. }
  873. return $config;
  874. }
  875. /**
  876. * Imports the configuration missing from the active store
  877. */
  878. protected function importMissing() {
  879. $config = $this->featuresManager->getConfigCollection();
  880. $missing = $this->featuresManager->reorderMissing($this->missing);
  881. foreach ($missing as $config_name) {
  882. if (!isset($config[$config_name])) {
  883. $item = $this->featuresManager->getConfigType($config_name);
  884. $type = ConfigurationItem::fromConfigStringToConfigType($item['type']);
  885. try {
  886. $this->configRevert->import($type, $item['name_short']);
  887. drupal_set_message($this->t('Imported @name', array('@name' => $config_name)));
  888. } catch (\Exception $e) {
  889. drupal_set_message($this->t('Error importing @name : @message',
  890. array('@name' => $config_name, '@message' => $e->getMessage())), 'error');
  891. }
  892. }
  893. }
  894. }
  895. /**
  896. * Updates the list of excluded config.
  897. *
  898. * @return array
  899. * The list of excluded config in a simple array of full config names
  900. * suitable for storing in the info.yml file.
  901. */
  902. protected function updateExcluded() {
  903. return $this->updateConstrained('excluded');
  904. }
  905. /**
  906. * Updates the list of required config.
  907. *
  908. * @return array
  909. * The list of required config in a simple array of full config names
  910. * suitable for storing in the info.yml file.
  911. */
  912. protected function updateRequired() {
  913. return $this->updateConstrained('required');
  914. }
  915. /**
  916. * Returns a list of constrained (excluded or required) configuration.
  917. *
  918. * @param string $constraint
  919. * The constraint (excluded or required).
  920. * @return array
  921. * The list of constrained config in a simple array of full config names
  922. * suitable for storing in the info.yml file.
  923. */
  924. protected function updateConstrained($constraint) {
  925. $constrained = array();
  926. foreach ($this->{$constraint} as $type => $item) {
  927. foreach ($item as $name => $value) {
  928. $constrained[] = $this->featuresManager->getFullName($type, $name);
  929. }
  930. }
  931. return $constrained;
  932. }
  933. /**
  934. * Encodes a given key.
  935. *
  936. * @param string $key
  937. * The key to encode.
  938. *
  939. * @return string
  940. * The encoded key.
  941. */
  942. protected function domEncode($key) {
  943. $replacements = $this->domEncodeMap();
  944. return strtr($key, $replacements);
  945. }
  946. /**
  947. * Decodes a given key.
  948. *
  949. * @param string $key
  950. * The key to decode.
  951. *
  952. * @return string
  953. * The decoded key.
  954. */
  955. protected function domDecode($key) {
  956. $replacements = array_flip($this->domEncodeMap());
  957. return strtr($key, $replacements);
  958. }
  959. /**
  960. * Decodes an array of option values that have been encoded by
  961. * features_dom_encode_options().
  962. *
  963. * @param array $options
  964. * The key to encode.
  965. * @param bool $keys_only
  966. * Whether to decode only the keys.
  967. *
  968. * @return array
  969. * An array of encoded options.
  970. */
  971. protected function domDecodeOptions(array $options, $keys_only = FALSE) {
  972. $replacements = array_flip($this->domEncodeMap());
  973. $encoded = array();
  974. foreach ($options as $key => $value) {
  975. $encoded[strtr($key, $replacements)] = $keys_only ? $value : strtr($value, $replacements);
  976. }
  977. return $encoded;
  978. }
  979. /**
  980. * Returns encoding map for decode and encode options.
  981. *
  982. * @return array
  983. * An encoding map.
  984. */
  985. protected function domEncodeMap() {
  986. return array(
  987. ':' => '__' . ord(':') . '__',
  988. '/' => '__' . ord('/') . '__',
  989. ',' => '__' . ord(',') . '__',
  990. '.' => '__' . ord('.') . '__',
  991. '<' => '__' . ord('<') . '__',
  992. '>' => '__' . ord('>') . '__',
  993. '%' => '__' . ord('%') . '__',
  994. ')' => '__' . ord(')') . '__',
  995. '(' => '__' . ord('(') . '__',
  996. );
  997. }
  998. }