FeaturesManager.php 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. <?php
  2. namespace Drupal\features;
  3. use Drupal;
  4. use Drupal\Component\Serialization\Yaml;
  5. use Drupal\Component\Utility\NestedArray;
  6. use Drupal\Core\Config\ConfigFactoryInterface;
  7. use Drupal\Core\Config\ConfigManagerInterface;
  8. use Drupal\Core\Config\InstallStorage;
  9. use Drupal\Core\Config\StorageInterface;
  10. use Drupal\Core\Entity\EntityManagerInterface;
  11. use Drupal\Core\Entity\EntityTypeInterface;
  12. use Drupal\Core\Extension\Extension;
  13. use Drupal\Core\Extension\ExtensionDiscovery;
  14. use Drupal\Core\Extension\ModuleHandlerInterface;
  15. use Drupal\Core\StringTranslation\StringTranslationTrait;
  16. /**
  17. * The FeaturesManager provides helper functions for building packages.
  18. */
  19. class FeaturesManager implements FeaturesManagerInterface {
  20. use StringTranslationTrait;
  21. /**
  22. * The entity manager.
  23. *
  24. * @var \Drupal\Core\Entity\EntityManagerInterface
  25. */
  26. protected $entityManager;
  27. /**
  28. * The target storage.
  29. *
  30. * @var \Drupal\Core\Config\StorageInterface
  31. */
  32. protected $configStorage;
  33. /**
  34. * The extension storages.
  35. *
  36. * @var \Drupal\features\FeaturesExtensionStoragesInterface
  37. */
  38. protected $extensionStorages;
  39. /**
  40. * The configuration manager.
  41. *
  42. * @var \Drupal\Core\Config\ConfigManagerInterface
  43. */
  44. protected $configManager;
  45. /**
  46. * The configuration factory.
  47. *
  48. * @var \Drupal\Core\Config\ConfigFactoryInterface
  49. */
  50. protected $configFactory;
  51. /**
  52. * The module handler.
  53. *
  54. * @var \Drupal\Core\Extension\ModuleHandlerInterface
  55. */
  56. protected $moduleHandler;
  57. /**
  58. * The Features settings.
  59. *
  60. * @var array
  61. */
  62. protected $settings;
  63. /**
  64. * The app root.
  65. *
  66. * @var string
  67. */
  68. protected $root;
  69. /**
  70. * The configuration present on the site.
  71. *
  72. * @var \Drupal\features\ConfigurationItem[]
  73. */
  74. private $configCollection;
  75. /**
  76. * The packages to be generated.
  77. *
  78. * @var \Drupal\features\Package[]
  79. */
  80. protected $packages;
  81. /**
  82. * Whether the packages have been assigned a bundle prefix.
  83. *
  84. * @var boolean
  85. */
  86. protected $packagesPrefixed;
  87. /**
  88. * The package assigner.
  89. *
  90. * @var \Drupal\features\FeaturesAssigner
  91. */
  92. protected $assigner;
  93. /**
  94. * Cache module.features.yml data keyed by module name.
  95. *
  96. * @var array
  97. */
  98. protected $featureInfoCache;
  99. /**
  100. * Constructs a FeaturesManager object.
  101. *
  102. * @param string $root
  103. * The app root.
  104. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
  105. * The entity manager.
  106. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  107. * The configuration factory.
  108. * @param \Drupal\Core\Config\StorageInterface $config_storage
  109. * The target storage.
  110. * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
  111. * The configuration manager.
  112. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  113. * The module handler.
  114. */
  115. public function __construct($root, EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory,
  116. StorageInterface $config_storage, ConfigManagerInterface $config_manager,
  117. ModuleHandlerInterface $module_handler) {
  118. $this->root = $root;
  119. $this->entityManager = $entity_manager;
  120. $this->configStorage = $config_storage;
  121. $this->configManager = $config_manager;
  122. $this->moduleHandler = $module_handler;
  123. $this->configFactory = $config_factory;
  124. $this->settings = $config_factory->getEditable('features.settings');
  125. $this->extensionStorages = new FeaturesExtensionStorages($this->configStorage);
  126. $this->extensionStorages->addStorage(InstallStorage::CONFIG_INSTALL_DIRECTORY);
  127. $this->extensionStorages->addStorage(InstallStorage::CONFIG_OPTIONAL_DIRECTORY);
  128. $this->packages = [];
  129. $this->packagesPrefixed = FALSE;
  130. $this->configCollection = [];
  131. }
  132. /**
  133. * {@inheritdoc}
  134. */
  135. public function getActiveStorage() {
  136. return $this->configStorage;
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. public function getExtensionStorages() {
  142. return $this->extensionStorages;
  143. }
  144. /**
  145. * {@inheritdoc}
  146. */
  147. public function getFullName($type, $name) {
  148. if ($type == FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG || !$type) {
  149. return $name;
  150. }
  151. $definition = $this->entityManager->getDefinition($type);
  152. $prefix = $definition->getConfigPrefix() . '.';
  153. return $prefix . $name;
  154. }
  155. /**
  156. * {@inheritdoc}
  157. */
  158. public function getConfigType($fullname) {
  159. $result = array(
  160. 'type' => '',
  161. 'name_short' => '',
  162. );
  163. $prefix = FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG . '.';
  164. if (strpos($fullname, $prefix)) {
  165. $result['type'] = FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG;
  166. $result['name_short'] = substr($fullname, strlen($prefix));
  167. }
  168. else {
  169. foreach ($this->entityManager->getDefinitions() as $entity_type => $definition) {
  170. if ($definition->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) {
  171. $prefix = $definition->getConfigPrefix() . '.';
  172. if (strpos($fullname, $prefix) === 0) {
  173. $result['type'] = $entity_type;
  174. $result['name_short'] = substr($fullname, strlen($prefix));
  175. }
  176. }
  177. }
  178. }
  179. return $result;
  180. }
  181. /**
  182. * {@inheritdoc}
  183. */
  184. public function reset() {
  185. $this->packages = [];
  186. // Don't use getConfigCollection because reset() may be called in
  187. // cases where we don't need to load config.
  188. foreach ($this->configCollection as $config) {
  189. $config->setPackage(NULL);
  190. }
  191. }
  192. /**
  193. * {@inheritdoc}
  194. */
  195. public function getConfigCollection($reset = FALSE) {
  196. $this->initConfigCollection($reset);
  197. return $this->configCollection;
  198. }
  199. /**
  200. * {@inheritdoc}
  201. */
  202. public function setConfigCollection(array $config_collection) {
  203. $this->configCollection = $config_collection;
  204. }
  205. /**
  206. * {@inheritdoc}
  207. */
  208. public function getPackages() {
  209. return $this->packages;
  210. }
  211. /**
  212. * {@inheritdoc}
  213. */
  214. public function setPackages(array $packages) {
  215. $this->packages = $packages;
  216. }
  217. /**
  218. * {@inheritdoc}
  219. */
  220. public function getPackage($machine_name) {
  221. if (isset($this->packages[$machine_name])) {
  222. return $this->packages[$machine_name];
  223. }
  224. return NULL;
  225. }
  226. /**
  227. * {@inheritdoc}
  228. */
  229. public function findPackage($machine_name) {
  230. $result = $this->getPackage($machine_name);
  231. if (!isset($result)) {
  232. // Didn't find direct match, but now go through and look for matching
  233. // full name (bundle_machinename)
  234. foreach ($this->packages as $name => $package) {
  235. if ($package->getFullName() == $machine_name) {
  236. return $this->packages[$name];
  237. }
  238. }
  239. }
  240. return $result;
  241. }
  242. /**
  243. * {@inheritdoc}
  244. */
  245. public function setPackage(Package $package) {
  246. if ($package->getMachineName()) {
  247. $this->packages[$package->getMachineName()] = $package;
  248. }
  249. }
  250. /**
  251. * {@inheritdoc}
  252. */
  253. public function filterPackages(array $packages, $namespace = '', $only_exported = FALSE) {
  254. $result = array();
  255. /** @var \Drupal\features\Package $package */
  256. foreach ($packages as $key => $package) {
  257. // A package matches the namespace if:
  258. // - it's prefixed with the namespace, or
  259. // - it's assigned to a bundle named for the namespace, or
  260. // - we're looking only for exported packages and it's not exported.
  261. if (empty($namespace) || (strpos($package->getMachineName(), $namespace . '_') === 0) ||
  262. ($package->getBundle() && $package->getBundle() === $namespace) ||
  263. ($only_exported && $package->getStatus() === FeaturesManagerInterface::STATUS_NO_EXPORT)) {
  264. $result[$key] = $package;
  265. }
  266. }
  267. return $result;
  268. }
  269. /**
  270. * {@inheritdoc}
  271. */
  272. public function getAssigner() {
  273. if (empty($this->assigner)) {
  274. $this->setAssigner(\Drupal::service('features_assigner'));
  275. }
  276. return $this->assigner;
  277. }
  278. /**
  279. * {@inheritdoc}
  280. */
  281. public function setAssigner(FeaturesAssignerInterface $assigner) {
  282. $this->assigner = $assigner;
  283. $this->reset();
  284. }
  285. /**
  286. * {@inheritdoc}
  287. */
  288. public function getGenerator() {
  289. return $this->generator;
  290. }
  291. /**
  292. * {@inheritdoc}
  293. */
  294. public function setGenerator(FeaturesGeneratorInterface $generator) {
  295. $this->generator = $generator;
  296. }
  297. /**
  298. * {@inheritdoc}
  299. */
  300. public function getExportSettings() {
  301. return $this->settings->get('export');
  302. }
  303. /**
  304. * {@inheritdoc}
  305. */
  306. public function getSettings() {
  307. return $this->settings;
  308. }
  309. /**
  310. * {@inheritdoc}
  311. */
  312. public function getExtensionInfo(Extension $extension) {
  313. return \Drupal::service('info_parser')->parse(\Drupal::root() . '/' . $extension->getPathname());
  314. }
  315. /**
  316. * {@inheritdoc}
  317. */
  318. public function isFeatureModule(Extension $module, FeaturesBundleInterface $bundle = NULL) {
  319. if ($features_info = $this->getFeaturesInfo($module)) {
  320. // If no bundle was requested, it's enough that this is a feature.
  321. if (is_null($bundle)) {
  322. return TRUE;
  323. }
  324. // If the default bundle was requested, look for features where
  325. // the bundle is not set.
  326. elseif ($bundle->isDefault()) {
  327. return !isset($features_info['bundle']);
  328. }
  329. // If we have a bundle name, look for it.
  330. else {
  331. return (isset($features_info['bundle']) && ($features_info['bundle'] == $bundle->getMachineName()));
  332. }
  333. }
  334. return FALSE;
  335. }
  336. /**
  337. * {@inheritdoc}
  338. */
  339. public function listPackageDirectories(array $machine_names = array(), FeaturesBundleInterface $bundle = NULL) {
  340. if (empty($machine_names)) {
  341. $machine_names = array_keys($this->getPackages());
  342. }
  343. // If the bundle is a profile, then add the profile's machine name.
  344. if (isset($bundle) && $bundle->isProfile() && !in_array($bundle->getProfileName(), $machine_names)) {
  345. $machine_names[] = $bundle->getProfileName();
  346. }
  347. $modules = $this->getFeaturesModules($bundle);
  348. // Filter to include only the requested packages.
  349. $modules = array_filter($modules, function ($module) use ($bundle, $machine_names) {
  350. $short_name = $bundle->getShortName($module->getName());
  351. return in_array($short_name, $machine_names);
  352. });
  353. $directories = array();
  354. foreach ($modules as $module) {
  355. $directories[$module->getName()] = $module->getPath();
  356. }
  357. return $directories;
  358. }
  359. /**
  360. * {@inheritdoc}
  361. */
  362. public function getAllModules() {
  363. static $modules;
  364. if (!isset($modules)) {
  365. // ModuleHandler::getModuleDirectories() returns data only for installed
  366. // modules. system_rebuild_module_data() includes only the site's install
  367. // profile directory, while we may need to include a custom profile.
  368. // @see _system_rebuild_module_data().
  369. $listing = new ExtensionDiscovery(\Drupal::root());
  370. $profile_directories = $listing->setProfileDirectoriesFromSettings()->getProfileDirectories();
  371. $installed_profile = $this->drupalGetProfile();
  372. if (isset($bundle) && $bundle->isProfile()) {
  373. $profile_directory = 'profiles/' . $bundle->getProfileName();
  374. if (($bundle->getProfileName() != $installed_profile) && is_dir($profile_directory)) {
  375. $profile_directories[] = $profile_directory;
  376. }
  377. }
  378. $listing->setProfileDirectories($profile_directories);
  379. // Find modules.
  380. $modules = $listing->scan('module');
  381. // Find installation profiles.
  382. $profiles = $listing->scan('profile');
  383. foreach ($profiles as $key => $profile) {
  384. $modules[$key] = $profile;
  385. }
  386. }
  387. return $modules;
  388. }
  389. /**
  390. * {@inheritdoc}
  391. */
  392. public function getFeaturesModules(FeaturesBundleInterface $bundle = NULL, $installed = FALSE) {
  393. $modules = $this->getAllModules();
  394. // Filter by bundle.
  395. if (isset($bundle)) {
  396. $features_manager = $this;
  397. $modules = array_filter($modules, function ($module) use ($features_manager, $bundle) {
  398. return $features_manager->isFeatureModule($module, $bundle);
  399. });
  400. }
  401. else {
  402. // No bundle filter, but still only return "Feature" modules
  403. $features_manager = $this;
  404. $modules = array_filter($modules, function ($module) use ($features_manager) {
  405. return $features_manager->isFeatureModule($module);
  406. });
  407. }
  408. // Filtered by installed status.
  409. if ($installed) {
  410. $features_manager = $this;
  411. $modules = array_filter($modules, function ($extension) use ($features_manager) {
  412. return $features_manager->extensionEnabled($extension);
  413. });
  414. }
  415. return $modules;
  416. }
  417. /**
  418. * {@inheritdoc}
  419. */
  420. public function extensionEnabled(Extension $extension) {
  421. return $this->moduleHandler->moduleExists($extension->getName());
  422. }
  423. /**
  424. * {@inheritdoc}
  425. */
  426. public function initPackage($machine_name, $name = NULL, $description = '', $type = 'module', FeaturesBundleInterface $bundle = NULL, Extension $extension = NULL) {
  427. if (!isset($this->packages[$machine_name])) {
  428. return $this->packages[$machine_name] = $this->getPackageObject($machine_name, $name, $description, $type, $bundle, $extension);
  429. }
  430. return $this->packages[$machine_name];
  431. }
  432. /**
  433. * {@inheritdoc}
  434. */
  435. public function initPackageFromExtension(Extension $extension) {
  436. $info = $this->getExtensionInfo($extension);
  437. $features_info = $this->getFeaturesInfo($extension);
  438. $bundle = $this->getAssigner()->findBundle($info, $features_info);
  439. // Use the full extension name as the short_name. Important to allow
  440. // multiple modules with different namespaces such as oa_media, test_media.
  441. $short_name = $extension->getName();
  442. return $this->initPackage($short_name, $info['name'], !empty($info['description']) ? $info['description'] : '', $info['type'], $bundle, $extension);
  443. }
  444. /**
  445. * Helper function to update dependencies array for a specific config item
  446. * @param \Drupal\features\ConfigurationItem $config a config item
  447. * @param array $module_list
  448. * @return array $dependencies
  449. */
  450. protected function getConfigDependency(ConfigurationItem $config, $module_list = array()) {
  451. $dependencies = [];
  452. $type = $config->getType();
  453. if ($type != FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG) {
  454. $provider = $this->entityManager->getDefinition($type)
  455. ->getProvider();
  456. // Ensure the provider is an installed module and not, for example, 'core'
  457. if (isset($module_list[$provider])) {
  458. $dependencies[] = $provider;
  459. }
  460. // For configuration in the InstallStorage::CONFIG_INSTALL_DIRECTORY
  461. // directory, set any module dependencies of the configuration item
  462. // as package dependencies.
  463. // As its name implies, the core-provided
  464. // InstallStorage::CONFIG_OPTIONAL_DIRECTORY should not create
  465. // dependencies.
  466. if ($config->getSubdirectory() === InstallStorage::CONFIG_INSTALL_DIRECTORY &&
  467. isset($config->getData()['dependencies']['module'])
  468. ) {
  469. $dependencies = array_merge($dependencies, $config->getData()['dependencies']['module']);
  470. }
  471. }
  472. return $dependencies;
  473. }
  474. /**
  475. * {@inheritdoc}
  476. */
  477. public function assignConfigPackage($package_name, array $item_names, $force = FALSE) {
  478. $config_collection = $this->getConfigCollection();
  479. $module_list = $this->moduleHandler->getModuleList();
  480. $packages =& $this->packages;
  481. if (isset($packages[$package_name])) {
  482. $package =& $packages[$package_name];
  483. }
  484. else {
  485. throw new \Exception($this->t('Failed to package @package_name. Package not found.', ['@package_name' => $package_name]));
  486. }
  487. foreach ($item_names as $item_name) {
  488. if (isset($config_collection[$item_name])) {
  489. // Add to the package if:
  490. // - force is set or
  491. // - the item hasn't already been assigned elsewhere, and
  492. // - the package hasn't been excluded.
  493. // - and the item isn't already in the package.
  494. $item = &$config_collection[$item_name];
  495. $already_assigned = !empty($item->getPackage());
  496. // If this is the profile package, we can reassign extension-provided configuration.
  497. $package_bundle = $this->getAssigner()->getBundle($package->getBundle());
  498. $is_profile_package = isset($package_bundle) ? $package_bundle->isProfilePackage($package_name) : FALSE;
  499. // An item is assignable if:
  500. // - it is not provider excluded or this is the profile package, and
  501. // - it is not flagged as excluded.
  502. $assignable = (!$item->isProviderExcluded() || $is_profile_package) && !$item->isExcluded();
  503. // An item is assignable if it was provided by the current package
  504. $assignable = $assignable || ($item->getProvider() == $package->getFullName());
  505. $excluded_from_package = in_array($package_name, $item->getPackageExcluded());
  506. $already_in_package = in_array($item_name, $package->getConfig());
  507. if (($force || (!$already_assigned && $assignable && !$excluded_from_package)) && !$already_in_package) {
  508. // Add the item to the package's config array.
  509. $package->appendConfig($item_name);
  510. // Mark the item as already assigned.
  511. $item->setPackage($package_name);
  512. $module_dependencies = $this->getConfigDependency($item, $module_list);
  513. $package->setDependencies($this->mergeUniqueItems($package->getDependencies(), $module_dependencies));
  514. }
  515. // Return memory
  516. unset($item);
  517. }
  518. }
  519. $this->setConfigCollection($config_collection);
  520. }
  521. /**
  522. * {@inheritdoc}
  523. */
  524. public function assignConfigByPattern(array $patterns) {
  525. // Regular expressions for items that are likely to generate false
  526. // positives when assigned by pattern.
  527. $false_positives = [
  528. // Blocks with the page title should not be assigned to a 'page' package.
  529. '/block\.block\..*_page_title/',
  530. ];
  531. $config_collection = $this->getConfigCollection();
  532. // Reverse sort by key so that child package will claim items before parent
  533. // package. E.g., event_registration will claim before event.
  534. krsort($config_collection);
  535. foreach ($patterns as $pattern => $machine_name) {
  536. if (isset($this->packages[$machine_name])) {
  537. foreach ($config_collection as $item_name => $item) {
  538. // Test for and skip false positives.
  539. foreach ($false_positives as $false_positive) {
  540. if (preg_match($false_positive, $item_name)) {
  541. continue 2;
  542. }
  543. }
  544. if (!$item->getPackage() && preg_match('/[_\-.]' . $pattern . '[_\-.]/', '.' . $item->getShortName() . '.')) {
  545. try {
  546. $this->assignConfigPackage($machine_name, [$item_name]);
  547. }
  548. catch (\Exception $exception) {
  549. \Drupal::logger('features')->error($exception->getMessage());
  550. }
  551. }
  552. }
  553. }
  554. }
  555. }
  556. /**
  557. * {@inheritdoc}
  558. */
  559. public function assignConfigDependents(array $item_names = NULL, $package = NULL) {
  560. $config_collection = $this->getConfigCollection();
  561. if (empty($item_names)) {
  562. $item_names = array_keys($config_collection);
  563. }
  564. foreach ($item_names as $item_name) {
  565. // Make sure the extension provided item exists in the active
  566. // configuration storage.
  567. if (isset($config_collection[$item_name]) && $config_collection[$item_name]->getPackage()) {
  568. foreach ($config_collection[$item_name]->getDependents() as $dependent_item_name) {
  569. if (isset($config_collection[$dependent_item_name]) && (!empty($package) || empty($config_collection[$dependent_item_name]->getPackage()))) {
  570. try {
  571. $package_name = !empty($package) ? $package : $config_collection[$item_name]->getPackage();
  572. $this->assignConfigPackage($package_name, [$dependent_item_name]);
  573. }
  574. catch (\Exception $exception) {
  575. \Drupal::logger('features')->error($exception->getMessage());
  576. }
  577. }
  578. }
  579. }
  580. }
  581. }
  582. /**
  583. * {@inheritdoc}
  584. */
  585. public function setPackageBundleNames(FeaturesBundleInterface $bundle, array &$package_names = []) {
  586. $this->packagesPrefixed = TRUE;
  587. if (!$bundle->isDefault()) {
  588. $new_package_names = [];
  589. // Assign the selected bundle to the exports.
  590. $packages = $this->getPackages();
  591. if (empty($package_names)) {
  592. $package_names = array_keys($packages);
  593. }
  594. foreach ($package_names as $package_name) {
  595. // Rename package to use bundle prefix.
  596. $package = $packages[$package_name];
  597. // The install profile doesn't need renaming.
  598. if ($package->getType() != 'profile') {
  599. unset($packages[$package_name]);
  600. $package->setMachineName($bundle->getFullName($package->getMachineName()));
  601. $packages[$package->getMachineName()] = $package;
  602. }
  603. // Set the bundle machine name.
  604. $packages[$package->getMachineName()]->setBundle($bundle->getMachineName());
  605. $new_package_names[] = $package->getMachineName();
  606. }
  607. $this->setPackages($packages);
  608. $package_names = $new_package_names;
  609. }
  610. }
  611. /**
  612. * {@inheritdoc}
  613. */
  614. public function assignPackageDependencies(Package $package = NULL) {
  615. if (is_null($package)) {
  616. $packages = $this->getPackages();
  617. }
  618. else {
  619. $packages = array($package);
  620. }
  621. $module_list = $this->moduleHandler->getModuleList();
  622. $config_collection = $this->getConfigCollection();
  623. foreach ($packages as $package) {
  624. $module_dependencies = [];
  625. foreach ($package->getConfig() as $item_name) {
  626. if (isset($config_collection[$item_name])) {
  627. $dependencies = $this->getConfigDependency($config_collection[$item_name], $module_list);
  628. $module_dependencies = array_merge($module_dependencies, $dependencies);
  629. }
  630. }
  631. $package->setDependencies($this->mergeUniqueItems($package->getDependencies(), $module_dependencies));
  632. }
  633. }
  634. /**
  635. * {@inheritdoc}
  636. */
  637. public function assignInterPackageDependencies(FeaturesBundleInterface $bundle, array &$packages) {
  638. if (!$this->packagesPrefixed) {
  639. throw new \Exception($this->t('The packages have not yet been prefixed with a bundle name.'));
  640. }
  641. $config_collection = $this->getConfigCollection();
  642. /** @var \Drupal\features\Package[] $packages */
  643. foreach ($packages as $package) {
  644. foreach ($package->getConfig() as $item_name) {
  645. if (!empty($config_collection[$item_name]->getData()['dependencies']['config'])) {
  646. foreach ($config_collection[$item_name]->getData()['dependencies']['config'] as $dependency_name) {
  647. if (isset($config_collection[$dependency_name])) {
  648. // If the required item is assigned to one of the packages, add
  649. // a dependency on that package.
  650. $dependency_set = FALSE;
  651. if ($dependency_package = $config_collection[$dependency_name]->getPackage()) {
  652. $package_name = $bundle->getFullName($dependency_package);
  653. // Package shouldn't be dependent on itself.
  654. if ($package_name && array_key_exists($package_name, $packages) && $package_name != $package->getMachineName()) {
  655. $package->setDependencies($this->mergeUniqueItems($package->getDependencies(), [$package_name]));
  656. $dependency_set = TRUE;
  657. }
  658. }
  659. // Otherwise, if the dependency is provided by an existing
  660. // feature, add a dependency on that feature.
  661. if (!$dependency_set && $extension_name = $config_collection[$dependency_name]->getProvider()) {
  662. // No extension should depend on the install profile.
  663. $package_name = $bundle->getFullName($package->getMachineName());
  664. if ($extension_name != $package_name && $extension_name != $this->drupalGetProfile()) {
  665. $package->setDependencies($this->mergeUniqueItems($package->getDependencies(), [$extension_name]));
  666. }
  667. }
  668. }
  669. }
  670. }
  671. }
  672. }
  673. // Unset the $package pass by reference.
  674. unset($package);
  675. }
  676. /**
  677. * Gets the name of the currently active installation profile.
  678. *
  679. * @return string|null $profile
  680. * The name of the installation profile or NULL if no installation profile is
  681. * currently active. This is the case for example during the first steps of
  682. * the installer or during unit tests.
  683. */
  684. protected function drupalGetProfile() {
  685. return drupal_get_profile();
  686. }
  687. /**
  688. * Merges a set of new item into an array and sorts the result.
  689. *
  690. * Only unique values are retained.
  691. *
  692. * @param array $items
  693. * An array of items.
  694. * @param array $new_items
  695. * An array of new items to be merged in.
  696. *
  697. * @return array
  698. * The merged, sorted and unique items.
  699. */
  700. protected function mergeUniqueItems($items, $new_items) {
  701. $items = array_unique(array_merge($items, $new_items));
  702. sort($items);
  703. return $items;
  704. }
  705. /**
  706. * Initializes and returns a package or profile array.
  707. *
  708. * @param string $machine_name
  709. * Machine name of the package.
  710. * @param string $name
  711. * (optional) Human readable name of the package.
  712. * @param string $description
  713. * (optional) Description of the package.
  714. * @param string $type
  715. * (optional) Type of project.
  716. * @param \Drupal\features\FeaturesBundleInterface $bundle
  717. * (optional) Bundle to use to add profile directories to the scan.
  718. * @param \Drupal\Core\Extension\Extension $extension
  719. * (optional) An Extension object.
  720. *
  721. * @return \Drupal\features\Package
  722. * An array of package properties; see
  723. * FeaturesManagerInterface::getPackages().
  724. */
  725. protected function getPackageObject($machine_name, $name = NULL, $description = '', $type = 'module', FeaturesBundleInterface $bundle = NULL, Extension $extension = NULL) {
  726. if (!isset($bundle)) {
  727. $bundle = $this->getAssigner()->getBundle();
  728. }
  729. $package = new Package($machine_name, [
  730. 'name' => isset($name) ? $name : ucwords(str_replace(['_', '-'], ' ', $machine_name)),
  731. 'description' => $description,
  732. 'type' => $type,
  733. 'core' => Drupal::CORE_COMPATIBILITY,
  734. 'dependencies' => [],
  735. 'themes' => [],
  736. 'config' => [],
  737. 'status' => FeaturesManagerInterface::STATUS_DEFAULT,
  738. 'version' => '',
  739. 'state' => FeaturesManagerInterface::STATE_DEFAULT,
  740. 'files' => [],
  741. 'bundle' => $bundle->isDefault() ? '' : $bundle->getMachineName(),
  742. 'extension' => NULL,
  743. 'info' => [],
  744. 'configOrig' => [],
  745. ]);
  746. // If no extension was passed in, look for a match.
  747. if (!isset($extension)) {
  748. $module_list = $this->getFeaturesModules($bundle);
  749. $full_name = $bundle->getFullName($package->getMachineName());
  750. if (isset($module_list[$full_name])) {
  751. $extension = $module_list[$full_name];
  752. }
  753. }
  754. // If there is an extension, set extension-specific properties.
  755. if (isset($extension)) {
  756. $info = $this->getExtensionInfo($extension);
  757. $features_info = $this->getFeaturesInfo($extension);
  758. $package->setExtension($extension);
  759. $package->setInfo($info);
  760. $package->setFeaturesInfo($features_info);
  761. $package->setConfigOrig($this->listExtensionConfig($extension));
  762. $package->setStatus($this->extensionEnabled($extension)
  763. ? FeaturesManagerInterface::STATUS_INSTALLED
  764. : FeaturesManagerInterface::STATUS_UNINSTALLED);
  765. $package->setVersion(isset($info['version']) ? $info['version'] : '');
  766. }
  767. return $package;
  768. }
  769. /**
  770. * Generates and adds .info.yml files to a package.
  771. *
  772. * @param \Drupal\features\Package $package
  773. * The package.
  774. */
  775. protected function addInfoFile(Package $package) {
  776. $info = [
  777. 'name' => $package->getName(),
  778. 'description' => $package->getDescription(),
  779. 'type' => $package->getType(),
  780. 'core' => $package->getCore(),
  781. 'dependencies' => $package->getDependencies(),
  782. 'themes' => $package->getThemes(),
  783. 'version' => $package->getVersion(),
  784. ];
  785. $features_info = [];
  786. // Assign to a "package" named for the profile.
  787. if ($package->getBundle()) {
  788. $bundle = $this->getAssigner()->getBundle($package->getBundle());
  789. }
  790. // Save the current bundle in the info file so the package
  791. // can be reloaded later by the AssignmentPackages plugin.
  792. if (isset($bundle) && !$bundle->isDefault()) {
  793. $info['package'] = $bundle->getName();
  794. $features_info['bundle'] = $bundle->getMachineName();
  795. }
  796. else {
  797. unset($features_info['bundle']);
  798. }
  799. if ($package->getConfig()) {
  800. foreach (array('excluded', 'required') as $constraint) {
  801. if (!empty($package->{'get' . $constraint}())) {
  802. $features_info[$constraint] = $package->{'get' . $constraint}();
  803. }
  804. else {
  805. unset($features_info[$constraint]);
  806. }
  807. }
  808. if (empty($features_info)) {
  809. $features_info = TRUE;
  810. }
  811. }
  812. // The name and description need to be cast as strings from the
  813. // TranslatableMarkup objects returned by t() to avoid raising an
  814. // InvalidDataTypeException on Yaml serialization.
  815. foreach (array('name', 'description') as $key) {
  816. $info[$key] = (string) $info[$key];
  817. }
  818. // Add profile-specific info data.
  819. if ($info['type'] == 'profile') {
  820. // Set the distribution name.
  821. $info['distribution'] = [
  822. 'name' => $info['name']
  823. ];
  824. }
  825. $package->appendFile([
  826. 'filename' => $package->getMachineName() . '.info.yml',
  827. 'subdirectory' => NULL,
  828. // Filter to remove any empty keys, e.g., an empty themes array.
  829. 'string' => Yaml::encode(array_filter($info))
  830. ], 'info');
  831. $package->appendFile([
  832. 'filename' => $package->getMachineName() . '.features.yml',
  833. 'subdirectory' => NULL,
  834. 'string' => Yaml::encode($features_info)
  835. ], 'features');
  836. }
  837. /**
  838. * Generates and adds files to a given package or profile.
  839. */
  840. protected function addPackageFiles(Package $package) {
  841. $config_collection = $this->getConfigCollection();
  842. // Only add files if there is at least one piece of configuration
  843. // present.
  844. if ($package->getConfig()) {
  845. // Add .info.yml files.
  846. $this->addInfoFile($package);
  847. // Add configuration files.
  848. foreach ($package->getConfig() as $name) {
  849. $config = $config_collection[$name];
  850. $data = $config->getData();
  851. // The _core is site-specific, so don't export it.
  852. unset($data['_core']);
  853. // The UUID is site-specfic, so don't export it.
  854. if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) {
  855. unset($data['uuid']);
  856. }
  857. $config->setData($data);
  858. // User roles include all permissions currently assigned to them. To
  859. // avoid extraneous additions, reset permissions.
  860. if ($config->getType() == 'user_role') {
  861. $data = $config->getData();
  862. // Unset and not empty permissions data to prevent loss of configured
  863. // role permissions in the event of a feature revert.
  864. unset($data['permissions']);
  865. $config->setData($data);
  866. }
  867. $package->appendFile([
  868. 'filename' => $config->getName() . '.yml',
  869. 'subdirectory' => $config->getSubdirectory(),
  870. 'string' => Yaml::encode($config->getData())
  871. ], $name);
  872. }
  873. }
  874. }
  875. /**
  876. * {@inheritdoc}
  877. */
  878. public function mergeInfoArray(array $info1, array $info2, array $keys = array()) {
  879. // If keys were specified, use only those.
  880. if (!empty($keys)) {
  881. $info2 = array_intersect_key($info2, array_fill_keys($keys, NULL));
  882. }
  883. $info = NestedArray::mergeDeep($info1, $info2);
  884. // Process the dependencies and themes keys.
  885. $keys = ['dependencies', 'themes'];
  886. foreach ($keys as $key) {
  887. if (isset($info[$key]) && is_array($info[$key])) {
  888. // NestedArray::mergeDeep() may produce duplicate values.
  889. $info[$key] = array_unique($info[$key]);
  890. sort($info[$key]);
  891. }
  892. }
  893. return $info;
  894. }
  895. /**
  896. * {@inheritdoc}
  897. */
  898. public function listConfigTypes($bundles_only = FALSE) {
  899. $definitions = [];
  900. foreach ($this->entityManager->getDefinitions() as $entity_type => $definition) {
  901. if ($definition->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) {
  902. if (!$bundles_only || $definition->getBundleOf()) {
  903. $definitions[$entity_type] = $definition;
  904. }
  905. }
  906. }
  907. $entity_types = array_map(function (EntityTypeInterface $definition) {
  908. return $definition->getLabel();
  909. }, $definitions);
  910. // Sort the entity types by label, then add the simple config to the top.
  911. uasort($entity_types, 'strnatcasecmp');
  912. return $bundles_only ? $entity_types : [
  913. FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG => $this->t('Simple configuration'),
  914. ] + $entity_types;
  915. }
  916. /**
  917. * {@inheritdoc}
  918. */
  919. public function listExtensionConfig(Extension $extension) {
  920. return $this->extensionStorages->listExtensionConfig($extension);
  921. }
  922. /**
  923. * {@inheritdoc}
  924. */
  925. public function listExistingConfig($installed = FALSE, FeaturesBundleInterface $bundle = NULL) {
  926. $config = array();
  927. $existing = $this->getFeaturesModules($bundle, $installed);
  928. foreach ($existing as $extension) {
  929. // Keys are configuration item names and values are providing extension
  930. // name.
  931. $new_config = array_fill_keys($this->listExtensionConfig($extension), $extension->getName());
  932. $config = array_merge($config, $new_config);
  933. }
  934. return $config;
  935. }
  936. /**
  937. * {@inheritdoc}
  938. */
  939. public function listConfigByType($config_type) {
  940. // For a given entity type, load all entities.
  941. if ($config_type && $config_type !== FeaturesManagerInterface::SYSTEM_SIMPLE_CONFIG) {
  942. $entity_storage = $this->entityManager->getStorage($config_type);
  943. $names = [];
  944. foreach ($entity_storage->loadMultiple() as $entity) {
  945. $entity_id = $entity->id();
  946. $label = $entity->label() ?: $entity_id;
  947. $names[$entity_id] = $label;
  948. }
  949. }
  950. // Handle simple configuration.
  951. else {
  952. $definitions = [];
  953. foreach ($this->entityManager->getDefinitions() as $entity_type => $definition) {
  954. if ($definition->isSubclassOf('Drupal\Core\Config\Entity\ConfigEntityInterface')) {
  955. $definitions[$entity_type] = $definition;
  956. }
  957. }
  958. // Gather the config entity prefixes.
  959. $config_prefixes = array_map(function (EntityTypeInterface $definition) {
  960. return $definition->getConfigPrefix() . '.';
  961. }, $definitions);
  962. // Find all config, and then filter our anything matching a config prefix.
  963. $names = $this->configStorage->listAll();
  964. $names = array_combine($names, $names);
  965. foreach ($names as $item_name) {
  966. foreach ($config_prefixes as $config_prefix) {
  967. if (strpos($item_name, $config_prefix) === 0) {
  968. unset($names[$item_name]);
  969. }
  970. }
  971. }
  972. }
  973. return $names;
  974. }
  975. /**
  976. * Creates a high performant version of the ConfigDependencyManager.
  977. *
  978. * @return \Drupal\features\FeaturesConfigDependencyManager
  979. * A high performant version of the ConfigDependencyManager.
  980. *
  981. * @see \Drupal\Core\Config\Entity\ConfigDependencyManager
  982. */
  983. protected function getFeaturesConfigDependencyManager() {
  984. $dependency_manager = new FeaturesConfigDependencyManager();
  985. // Read all configuration using the factory. This ensures that multiple
  986. // deletes during the same request benefit from the static cache. Using the
  987. // factory also ensures configuration entity dependency discovery has no
  988. // dependencies on the config entity classes. Assume data with UUID is a
  989. // config entity. Only configuration entities can be depended on so we can
  990. // ignore everything else.
  991. $data = array_map(function(Drupal\Core\Config\ImmutableConfig $config) {
  992. $data = $config->get();
  993. if (isset($data['uuid'])) {
  994. return $data;
  995. }
  996. return FALSE;
  997. }, $this->configFactory->loadMultiple($this->configStorage->listAll()));
  998. $dependency_manager->setData(array_filter($data));
  999. return $dependency_manager;
  1000. }
  1001. /**
  1002. * Loads configuration from storage into a property.
  1003. */
  1004. protected function initConfigCollection($reset = FALSE) {
  1005. if ($reset || empty($this->configCollection)) {
  1006. $config_collection = [];
  1007. $config_types = $this->listConfigTypes();
  1008. $dependency_manager = $this->getFeaturesConfigDependencyManager();
  1009. // List configuration provided by installed features.
  1010. $existing_config = $this->listExistingConfig(NULL);
  1011. foreach (array_keys($config_types) as $config_type) {
  1012. $config = $this->listConfigByType($config_type);
  1013. foreach ($config as $item_name => $label) {
  1014. $name = $this->getFullName($config_type, $item_name);
  1015. $data = $this->configStorage->read($name);
  1016. $config_collection[$name] = (new ConfigurationItem($name, $data, [
  1017. 'shortName' => $item_name,
  1018. 'label' => $label,
  1019. 'type' => $config_type,
  1020. 'dependents' => array_keys($dependency_manager->getDependentEntities('config', $name)),
  1021. // Default to the install directory.
  1022. 'subdirectory' => InstallStorage::CONFIG_INSTALL_DIRECTORY,
  1023. 'package' => '',
  1024. 'providerExcluded' => NULL,
  1025. 'provider' => isset($existing_config[$name]) ? $existing_config[$name] : NULL,
  1026. 'packageExcluded' => [],
  1027. ]));
  1028. }
  1029. }
  1030. $this->setConfigCollection($config_collection);
  1031. }
  1032. }
  1033. /**
  1034. * {@inheritdoc}
  1035. */
  1036. public function prepareFiles(array $packages) {
  1037. foreach ($packages as $package) {
  1038. $this->addPackageFiles($package);
  1039. }
  1040. }
  1041. /**
  1042. * {@inheritdoc}
  1043. */
  1044. public function getExportInfo(Package $package, FeaturesBundleInterface $bundle = NULL) {
  1045. $full_name = isset($bundle) ? $bundle->getFullName($package->getMachineName()) : $package->getMachineName();
  1046. $path = '';
  1047. // Adjust export directory to be in profile.
  1048. if (isset($bundle) && $bundle->isProfile()) {
  1049. $path .= 'profiles/' . $bundle->getProfileName();
  1050. }
  1051. // If this is not the profile package, nest the directory.
  1052. if (!isset($bundle) || !$bundle->isProfilePackage($package->getMachineName())) {
  1053. $path .= empty($path) ? 'modules' : '/modules';
  1054. $export_settings = $this->getExportSettings();
  1055. if (!empty($export_settings['folder'])) {
  1056. $path .= '/' . $export_settings['folder'];
  1057. }
  1058. }
  1059. // Use the same path of a package to override it.
  1060. if ($extension = $package->getExtension()) {
  1061. $extension_path = $extension->getPath();
  1062. $path = dirname($extension_path);
  1063. }
  1064. return array($full_name, $path);
  1065. }
  1066. /**
  1067. * {@inheritdoc}
  1068. */
  1069. public function detectOverrides(Package $feature, $include_new = FALSE) {
  1070. /** @var \Drupal\config_update\ConfigDiffInterface $config_diff */
  1071. $config_diff = \Drupal::service('config_update.config_diff');
  1072. $different = array();
  1073. foreach ($feature->getConfig() as $name) {
  1074. $active = $this->configStorage->read($name);
  1075. $extension = $this->extensionStorages->read($name);
  1076. $extension = !empty($extension) ? $extension : array();
  1077. if (($include_new || !empty($extension)) && !$config_diff->same($extension, $active)) {
  1078. $different[] = $name;
  1079. }
  1080. }
  1081. if (!empty($different)) {
  1082. $feature->setState(FeaturesManagerInterface::STATE_OVERRIDDEN);
  1083. }
  1084. return $different;
  1085. }
  1086. /**
  1087. * {@inheritdoc}
  1088. */
  1089. public function detectNew(Package $feature) {
  1090. $result = array();
  1091. foreach ($feature->getConfig() as $name) {
  1092. $extension = $this->extensionStorages->read($name);
  1093. if (empty($extension)) {
  1094. $result[] = $name;
  1095. }
  1096. }
  1097. return $result;
  1098. }
  1099. /**
  1100. * {@inheritdoc}
  1101. */
  1102. public function detectMissing(Package $feature) {
  1103. $config = $this->getConfigCollection();
  1104. $result = array();
  1105. foreach ($feature->getConfigOrig() as $name) {
  1106. if (!isset($config[$name])) {
  1107. $result[] = $name;
  1108. }
  1109. }
  1110. return $result;
  1111. }
  1112. /**
  1113. * {@inheritdoc}
  1114. */
  1115. public function reorderMissing(array $missing) {
  1116. $list = array();
  1117. $result = array();
  1118. foreach ($missing as $full_name) {
  1119. $this->addConfigList($full_name, $list);
  1120. }
  1121. foreach ($list as $full_name) {
  1122. if (in_array($full_name, $missing)) {
  1123. $result[] = $full_name;
  1124. }
  1125. }
  1126. return $result;
  1127. }
  1128. protected function addConfigList($full_name, &$list) {
  1129. $index = array_search($full_name, $list);
  1130. if ($index !== FALSE) {
  1131. unset($list[$index]);
  1132. }
  1133. array_unshift($list, $full_name);
  1134. $value = $this->extensionStorages->read($full_name);
  1135. if (isset($value['dependencies']['config'])) {
  1136. foreach ($value['dependencies']['config'] as $config_name) {
  1137. $this->addConfigList($config_name, $list);
  1138. }
  1139. }
  1140. }
  1141. /**
  1142. * {@inheritdoc}
  1143. */
  1144. public function statusLabel($status) {
  1145. switch ($status) {
  1146. case FeaturesManagerInterface::STATUS_NO_EXPORT:
  1147. return $this->t('Not exported');
  1148. case FeaturesManagerInterface::STATUS_UNINSTALLED:
  1149. return $this->t('Uninstalled');
  1150. case FeaturesManagerInterface::STATUS_INSTALLED:
  1151. return $this->t('Installed');
  1152. }
  1153. }
  1154. /**
  1155. * {@inheritdoc}
  1156. */
  1157. public function stateLabel($state) {
  1158. switch ($state) {
  1159. case FeaturesManagerInterface::STATE_DEFAULT:
  1160. return $this->t('Default');
  1161. case FeaturesManagerInterface::STATE_OVERRIDDEN:
  1162. return $this->t('Changed');
  1163. }
  1164. }
  1165. /**
  1166. * {@inheritdoc}
  1167. */
  1168. public function getFeaturesInfo(Extension $extension) {
  1169. $module_name = $extension->getName();
  1170. if (isset($this->featureInfoCache[$module_name])) {
  1171. return $this->featureInfoCache[$module_name];
  1172. }
  1173. $features_info = NULL;
  1174. $filename = $this->root . '/' . $extension->getPath() . '/' . $module_name . '.features.yml';
  1175. if (file_exists($filename)) {
  1176. $features_info = Yaml::decode(file_get_contents($filename));
  1177. }
  1178. $this->featureInfoCache[$module_name] = $features_info;
  1179. return $features_info;
  1180. }
  1181. }