/** * Registers a failed package or profile archive operation. * * @param array &$return * The return value, passed by reference. * @param \Drupal\features\Package $package * The package or profile. * @param \Exception $exception * The exception object. * @param string $message * Error message when there isn't an Exception object. */ protected function failure(array &$return, Package $package, \Exception $exception = NULL, $message = '') { $type = $package->getType() == 'module' ? $this->t('Package') : $this->t('Profile'); $return[] = ['success' => FALSE, 'display' => FALSE, 'message' => '@type @package not written to archive. Error: @error.', 'variables' => ['@type' => $type, '@package' => $package->getName(), '@error' => isset($exception) ? $exception->getMessage() : $message]]; }
/** * Builds the details of a package. * * @param \Drupal\features\Package $package * The package. * * @return array * A render array of a form element. */ protected function buildPackageDetail(Package $package) { $config_collection = $this->featuresManager->getConfigCollection(); $url = Url::fromRoute('features.edit', array('featurename' => $package->getMachineName())); $element['name'] = array('data' => \Drupal::l($package->getName(), $url), 'class' => array('feature-name')); $machine_name = $package->getMachineName(); // Except for the 'unpackaged' pseudo-package, display the full name, since // that's what will be generated. if ($machine_name !== 'unpackaged') { $machine_name = $this->assigner->getBundle($package->getBundle())->getFullName($machine_name); } $element['machine_name'] = $machine_name; $element['status'] = array('data' => $this->featuresManager->statusLabel($package->getStatus()), 'class' => array('column-nowrap')); // Use 'data' instead of plain string value so a blank version doesn't // remove column from table. $element['version'] = array('data' => SafeMarkup::checkPlain($package->getVersion()), 'class' => array('column-nowrap')); $overrides = $this->featuresManager->detectOverrides($package); $new_config = $this->featuresManager->detectNew($package); $conflicts = array(); $missing = array(); if ($package->getStatus() == FeaturesManagerInterface::STATUS_NO_EXPORT) { $overrides = array(); $new_config = array(); } // Bundle package configuration by type. $package_config = array(); foreach ($package->getConfig() as $item_name) { $item = $config_collection[$item_name]; $package_config[$item->getType()][] = array('name' => SafeMarkup::checkPlain($item_name), 'label' => SafeMarkup::checkPlain($item->getLabel()), 'class' => in_array($item_name, $overrides) ? 'features-override' : (in_array($item_name, $new_config) ? 'features-detected' : '')); } // Conflict config from other modules. foreach ($package->getConfigOrig() as $item_name) { if (!isset($config_collection[$item_name])) { $missing[] = $item_name; $package_config['missing'][] = array('name' => SafeMarkup::checkPlain($item_name), 'label' => SafeMarkup::checkPlain($item_name), 'class' => 'features-conflict'); } elseif (!in_array($item_name, $package->getConfig())) { $item = $config_collection[$item_name]; $conflicts[] = $item_name; $package_config[$item->getType()][] = array('name' => SafeMarkup::checkPlain($item_name), 'label' => SafeMarkup::checkPlain($item->getLabel()), 'class' => 'features-conflict'); } } // Add dependencies. $package_config['dependencies'] = array(); foreach ($package->getDependencies() as $dependency) { $package_config['dependencies'][] = array('name' => $dependency, 'label' => $this->moduleHandler->getName($dependency), 'class' => ''); } $class = ''; $label = ''; if (!empty($conflicts)) { $url = Url::fromRoute('features.edit', array('featurename' => $package->getMachineName())); $class = 'features-conflict'; $label = $this->t('Conflicts'); } elseif (!empty($overrides)) { $url = Url::fromRoute('features.diff', array('featurename' => $package->getMachineName())); $class = 'features-override'; $label = $this->featuresManager->stateLabel(FeaturesManagerInterface::STATE_OVERRIDDEN); } elseif (!empty($new_config)) { $url = Url::fromRoute('features.diff', array('featurename' => $package->getMachineName())); $class = 'features-detected'; $label = $this->t('New detected'); } elseif (!empty($missing)) { $url = Url::fromRoute('features.edit', array('featurename' => $package->getMachineName())); $class = 'features-conflict'; $label = $this->t('Missing'); } if (!empty($class)) { $element['state'] = array('data' => \Drupal::l($label, $url), 'class' => array($class, 'column-nowrap')); } else { $element['state'] = ''; } $config_types = $this->featuresManager->listConfigTypes(); // Add dependencies. $config_types['dependencies'] = $this->t('Dependencies'); $config_types['missing'] = $this->t('Missing'); uasort($config_types, 'strnatcasecmp'); $rows = array(); // Use sorted array for order. foreach ($config_types as $type => $label) { // For each component type, offer alternating rows. $row = array(); if (isset($package_config[$type])) { $row[] = array('data' => array('#type' => 'html_tag', '#tag' => 'span', '#value' => SafeMarkup::checkPlain($label), '#attributes' => array('title' => SafeMarkup::checkPlain($type), 'class' => 'features-item-label'))); $row[] = array('data' => array('#theme' => 'features_items', '#items' => $package_config[$type], '#value' => SafeMarkup::checkPlain($label), '#title' => SafeMarkup::checkPlain($type)), 'class' => 'item'); $rows[] = $row; } } $element['table'] = array('#type' => 'table', '#rows' => $rows); $details = array(); $details['description'] = array('#markup' => Xss::filterAdmin($package->getDescription())); $details['table'] = array('#type' => 'details', '#title' => array('#markup' => $this->t('Included configuration')), '#description' => array('data' => $element['table'])); $element['details'] = array('class' => array('description', 'expand'), 'data' => $details); return $element; }
/** * Generates and adds .info.yml files to a package. * * @param \Drupal\features\Package $package * The package. */ protected function addInfoFile(Package $package) { $info = ['name' => $package->getName(), 'description' => $package->getDescription(), 'type' => $package->getType(), 'core' => $package->getCore(), 'dependencies' => $package->getDependencies(), 'themes' => $package->getThemes(), 'version' => $package->getVersion()]; $features_info = []; // Assign to a "package" named for the profile. if ($package->getBundle()) { $bundle = $this->getAssigner()->getBundle($package->getBundle()); } // Save the current bundle in the info file so the package // can be reloaded later by the AssignmentPackages plugin. if (isset($bundle) && !$bundle->isDefault()) { $info['package'] = $bundle->getName(); $features_info['bundle'] = $bundle->getMachineName(); } else { unset($features_info['bundle']); } if ($package->getConfig()) { foreach (array('excluded', 'required') as $constraint) { if (!empty($package->{'get' . $constraint}())) { $features_info[$constraint] = $package->{'get' . $constraint}(); } else { unset($features_info[$constraint]); } } if (empty($features_info)) { $features_info = TRUE; } } // The name and description need to be cast as strings from the // TranslatableMarkup objects returned by t() to avoid raising an // InvalidDataTypeException on Yaml serialization. foreach (array('name', 'description') as $key) { $info[$key] = (string) $info[$key]; } // Add profile-specific info data. if ($info['type'] == 'profile') { // Set the distribution name. $info['distribution'] = ['name' => $info['name']]; } $package->appendFile(['filename' => $package->getMachineName() . '.info.yml', 'subdirectory' => NULL, 'string' => Yaml::encode(array_filter($info))], 'info'); $package->appendFile(['filename' => $package->getMachineName() . '.features.yml', 'subdirectory' => NULL, 'string' => Yaml::encode($features_info)], 'features'); }
/** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, $featurename = '') { $session = $this->getRequest()->getSession(); $this->allowConflicts = FALSE; if (isset($session)) { $this->allowConflicts = $session->get('features_allow_conflicts', FALSE); } $trigger = $form_state->getTriggeringElement(); if ($trigger['#name'] == 'package') { $this->oldBundle = $this->bundle; $bundle_name = $form_state->getValue('package'); $bundle = $this->assigner->getBundle($bundle_name); } elseif ($trigger['#name'] == 'conflicts') { if (isset($session)) { $session->set('features_allow_conflicts', $form_state->getValue('conflicts')); } return $this->redirect('features.edit', array($featurename)); } else { $bundle = $this->assigner->loadBundle(); } $this->bundle = $bundle->getMachineName(); // Pass the $force argument as TRUE because we want to include any excluded // configuration items. These should show up as automatically assigned, but // not selected, thus allowing the admin to reselect if desired. // @see FeaturesManagerInterface::assignConfigPackage() $this->assigner->assignConfigPackages(TRUE); $packages = $this->featuresManager->getPackages(); if (empty($packages[$featurename])) { $featurename = str_replace(array('-', ' '), '_', $featurename); $this->package = $this->featuresManager->initPackage($featurename, NULL, '', 'module', $bundle); } else { $this->package = $packages[$featurename]; } $form = array('#show_operations' => FALSE, '#prefix' => '<div id="features-edit-wrapper">', '#suffix' => '</div>'); $form['info'] = array('#type' => 'fieldset', '#title' => t('General Information'), '#tree' => FALSE, '#weight' => 2, '#prefix' => "<div id='features-export-info'>", '#suffix' => '</div>'); $form['info']['name'] = array('#title' => t('Name'), '#description' => t('Example: Image gallery') . ' (' . t('Do not begin name with numbers.') . ')', '#type' => 'textfield', '#default_value' => $this->package->getName()); if (!$bundle->isDefault()) { $form['info']['name']['#description'] .= '<br/>' . t('The namespace "@name_" will be prepended to the machine name', array('@name' => $bundle->getMachineName())); } $form['info']['machine_name'] = array('#type' => 'machine_name', '#title' => t('Machine-readable name'), '#description' => t('Example: image_gallery') . ' ' . t('May only contain lowercase letters, numbers and underscores.'), '#required' => TRUE, '#default_value' => $bundle->getShortName($this->package->getMachineName()), '#machine_name' => array('source' => array('info', 'name'), 'exists' => array($this, 'featureExists'))); if (!$bundle->isDefault()) { $form['info']['machine_name']['#description'] .= '<br/>' . t('NOTE: Do NOT include the namespace prefix "@name_"; it will be added automatically.', array('@name' => $bundle->getMachineName())); } $form['info']['description'] = array('#title' => t('Description'), '#description' => t('Provide a short description of what users should expect when they install your feature.'), '#type' => 'textarea', '#rows' => 3, '#default_value' => $this->package->getDescription()); $form['info']['package'] = array('#title' => t('Bundle'), '#type' => 'select', '#options' => $this->assigner->getBundleOptions(t('--None--')), '#default_value' => $bundle->getMachineName(), '#ajax' => array('callback' => '::updateBundle', 'wrapper' => 'features-export-info')); $form['info']['version'] = array('#title' => t('Version'), '#description' => t('Examples: 8.x-1.0, 8.x-1.0-beta1'), '#type' => 'textfield', '#required' => FALSE, '#default_value' => $this->package->getVersion(), '#size' => 30); $form['conflicts'] = array('#type' => 'checkbox', '#title' => t('Allow conflicts'), '#default_value' => $this->allowConflicts, '#description' => $this->t('Allow configuration to be exported to more than one feature.'), '#weight' => 8, '#ajax' => array('callback' => '::updateForm', 'wrapper' => 'features-edit-wrapper')); $generation_info = array(); if (\Drupal::currentUser()->hasPermission('export configuration')) { // Offer available generation methods. $generation_info = $this->generator->getGenerationMethods(); // Sort generation methods by weight. uasort($generation_info, '\\Drupal\\Component\\Utility\\SortArray::sortByWeightElement'); } $form['actions'] = array('#type' => 'actions', '#tree' => TRUE); foreach ($generation_info as $method_id => $method) { $form['actions'][$method_id] = array('#type' => 'submit', '#name' => $method_id, '#value' => $this->t('@name', array('@name' => $method['name'])), '#attributes' => array('title' => SafeMarkup::checkPlain($method['description']))); } // Build the Component Listing panel on the right. $form['export'] = $this->buildComponentList($form_state); $form['#attached'] = array('library' => array('features_ui/drupal.features_ui.admin'), 'drupalSettings' => array('features' => array('excluded' => $this->excluded, 'required' => $this->required, 'conflicts' => $this->conflicts, 'autodetect' => TRUE))); return $form; }
/** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, $featurename = '') { $session = $this->getRequest()->getSession(); $trigger = $form_state->getTriggeringElement(); if ($trigger['#name'] == 'package') { // Save current bundle name for later ajax callback. $this->oldBundle = $this->bundle; } elseif ($trigger['#name'] == 'conflicts') { if (isset($session)) { $session->set('features_allow_conflicts', $form_state->getValue('conflicts')); } } if (!$form_state->isValueEmpty('package')) { $bundle_name = $form_state->getValue('package'); $bundle = $this->assigner->getBundle($bundle_name); } else { $bundle = $this->assigner->loadBundle(); } // Only store bundle name, not full object. $this->bundle = $bundle->getMachineName(); $this->allowConflicts = FALSE; if (isset($session)) { $this->allowConflicts = $session->get('features_allow_conflicts', FALSE); } // Pass the $force argument as TRUE because we want to include any excluded // configuration items. These should show up as automatically assigned, but // not selected, thus allowing the admin to reselect if desired. // @see FeaturesManagerInterface::assignConfigPackage() $this->assigner->assignConfigPackages(TRUE); $packages = $this->featuresManager->getPackages(); if (empty($packages[$featurename])) { $featurename = str_replace(array('-', ' '), '_', $featurename); $this->package = $this->featuresManager->initPackage($featurename, NULL, '', 'module', $bundle); } else { $this->package = $packages[$featurename]; } if (!empty($packages[$featurename]) && $this->package->getBundle() !== $this->bundle && $form_state->isValueEmpty('package')) { // Make sure the current bundle matches what is stored in the package. // But only do this if the Package value hasn't been manually changed. $bundle = $this->assigner->getBundle($this->package->getBundle()); if (empty($bundle)) { // Create bundle if it doesn't exist yet $bundle = $this->assigner->createBundleFromDefault($this->package->getBundle()); } $this->bundle = $bundle->getMachineName(); $this->assigner->reset(); $this->assigner->assignConfigPackages(TRUE); $packages = $this->featuresManager->getPackages(); $this->package = $packages[$featurename]; } $form = array('#show_operations' => FALSE, '#prefix' => '<div id="features-edit-wrapper">', '#suffix' => '</div>'); $form['info'] = array('#type' => 'fieldset', '#title' => $this->t('General Information'), '#tree' => FALSE, '#weight' => 2, '#prefix' => "<div id='features-export-info'>", '#suffix' => '</div>'); $form['info']['name'] = array('#title' => $this->t('Name'), '#description' => $this->t('Example: Image gallery') . ' (' . $this->t('Do not begin name with numbers.') . ')', '#type' => 'textfield', '#default_value' => $this->package->getName()); if (!$bundle->isDefault()) { $form['info']['name']['#description'] .= '<br/>' . $this->t('The namespace "@name_" will be prepended to the machine name', array('@name' => $bundle->getMachineName())); } $form['info']['machine_name'] = array('#type' => 'machine_name', '#title' => $this->t('Machine-readable name'), '#description' => $this->t('Example: image_gallery') . ' ' . $this->t('May only contain lowercase letters, numbers and underscores.'), '#required' => TRUE, '#default_value' => $bundle->getShortName($this->package->getMachineName()), '#machine_name' => array('source' => array('info', 'name'), 'exists' => array($this, 'featureExists'))); if (!$bundle->isDefault()) { $form['info']['machine_name']['#description'] .= '<br/>' . $this->t('NOTE: Do NOT include the namespace prefix "@name_"; it will be added automatically.', array('@name' => $bundle->getMachineName())); } $form['info']['description'] = array('#title' => $this->t('Description'), '#description' => $this->t('Provide a short description of what users should expect when they install your feature.'), '#type' => 'textarea', '#rows' => 3, '#default_value' => $this->package->getDescription()); $form['info']['package'] = array('#title' => $this->t('Bundle'), '#type' => 'select', '#options' => $this->assigner->getBundleOptions(), '#default_value' => $bundle->getMachineName(), '#ajax' => array('callback' => '::updateBundle', 'wrapper' => 'features-export-info')); $form['info']['version'] = array('#title' => $this->t('Version'), '#description' => $this->t('Examples: 8.x-1.0, 8.x-1.0-beta1'), '#type' => 'textfield', '#required' => FALSE, '#default_value' => $this->package->getVersion(), '#size' => 30); list($full_name, $path) = $this->featuresManager->getExportInfo($this->package, $bundle); $form['info']['directory'] = array('#title' => $this->t('Path'), '#description' => $this->t('Path to export package using Write action, relative to root directory.'), '#type' => 'textfield', '#required' => FALSE, '#default_value' => $path, '#size' => 30); $require_all = $this->package->getRequiredAll(); $form['info']['require_all'] = array('#type' => 'checkbox', '#title' => $this->t('Mark all config as required'), '#default_value' => $this->package->getRequiredAll(), '#description' => $this->t('Required config will be assigned to this feature regardless of other assignment plugins.')); $form['conflicts'] = array('#type' => 'checkbox', '#title' => $this->t('Allow conflicts'), '#default_value' => $this->allowConflicts, '#description' => $this->t('Allow configuration to be exported to more than one feature.'), '#weight' => 8, '#ajax' => array('callback' => '::updateForm', 'wrapper' => 'features-edit-wrapper')); $generation_info = array(); if (\Drupal::currentUser()->hasPermission('export configuration')) { // Offer available generation methods. $generation_info = $this->generator->getGenerationMethods(); // Sort generation methods by weight. uasort($generation_info, '\\Drupal\\Component\\Utility\\SortArray::sortByWeightElement'); } $form['actions'] = array('#type' => 'actions', '#tree' => TRUE); foreach ($generation_info as $method_id => $method) { $form['actions'][$method_id] = array('#type' => 'submit', '#name' => $method_id, '#value' => $this->t('@name', array('@name' => $method['name'])), '#attributes' => array('title' => Html::escape($method['description']))); } // Build the Component Listing panel on the right. $form['export'] = $this->buildComponentList($form_state); if (!empty($this->missing)) { if ($this->allowConflicts) { $form['actions']['#prefix'] = '<strong>' . $this->t('WARNING: Package contains configuration missing from site.') . '<br>' . $this->t('This configuration will be removed if you export it.') . '</strong>'; } else { foreach ($generation_info as $method_id => $method) { unset($form['actions'][$method_id]); } $form['actions']['#prefix'] = '<strong>' . $this->t('Package contains configuration missing from site.') . '<br>' . $this->t('Import the feature to create the missing config before you can export it.') . '<br>' . $this->t('Or, enable the Allow Conflicts option above.') . '</strong>'; } $form['actions']['import_missing'] = array('#type' => 'submit', '#name' => 'import_missing', '#value' => $this->t('Import Missing'), '#attributes' => array('title' => $this->t('Import only the missing configuration items.'))); } $form['#attached'] = array('library' => array('features_ui/drupal.features_ui.admin'), 'drupalSettings' => array('features' => array('excluded' => $this->excluded, 'required' => $this->required, 'conflicts' => $this->conflicts, 'autodetect' => TRUE))); return $form; }