Skip to content

Commit

Permalink
LDT-49: API Endpoint for Exporting Site Configurations solution.
Browse files Browse the repository at this point in the history
  • Loading branch information
axelabhay committed May 3, 2024
1 parent 219c837 commit 8ea607e
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Drupal/modules/custom/config_exporter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# API Endpoint for Exporting Site Configurations

## Description:
To develop a secure REST API endpoint that enables administrators to export crucial site configurations, including content types and field settings, thereby facilitating site management and configuration versioning.

## Acceptance Criteria:
- The endpoint must be secured with appropriate authentication mechanisms to ensure that only users with administrative privileges can access the export functionality.
- The API must provide a JSON export of selected configuration settings.
- Proper error responses should be implemented to handle unauthorized access, unsupported configuration requests, and other potential API errors.

## Solution
Course Link:
Troubleshoot:
Raise Issue: https://github.com/axelerant-trainings/project-usecases/issues/new
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: 'Configuration Exporter'
type: module
description: 'Provides a REST endpoint for exporting site configurations.'
core_version_requirement: ^10
package: Custom
dependencies:
- drupal:rest
- drupal:serialization
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
config_exporter.admin_settings:
path: '/admin/config/system/config-manager'
defaults:
_form: '\Drupal\config_exporter\Form\ConfigManagerForm'
_title: 'Configuration Manager'
requirements:
_permission: 'administer site configuration'
143 changes: 143 additions & 0 deletions Drupal/modules/custom/config_exporter/src/Form/ConfigManagerForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace Drupal\config_exporter\Form;

use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ConfigManagerForm extends ConfigFormBase {

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;

/**
* The config storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $configStorage;

/**
* Tracks the valid config entity type definitions.
*
* @var \Drupal\Core\Entity\EntityTypeInterface[]
*/
protected $definitions = [];

/**
* Constructs a new ConfigSingleImportForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The config storage.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, StorageInterface $config_storage) {
$this->entityTypeManager = $entity_type_manager;
$this->configStorage = $config_storage;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('config.storage')
);
}

/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'config_exporter.settings',
];
}

/**
* {@inheritdoc}
*/
public function getFormId() {
return 'config_exporter_admin_settings';
}

/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$options = $this->getListOfConfigOptions();
$selected = $this->config('config_exporter.settings')->get('selected_configurations') ?: [];

$form['configurations'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Choose configurations to expose via API'),
'#options' => $options,
'#default_value' => $selected,
];

return parent::buildForm($form, $form_state);
}

/**
* Returns a list of general configurations.
*
* @return array
* A list of general conficgurations.
*/
private function getListOfConfigOptions() {

// Entity related configurations.
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
if ($definition->entityClassImplements(ConfigEntityInterface::class)) {
$this->definitions[$entity_type] = $definition;
}
}

// Gather the config entity prefixes.
$config_prefixes = array_map(function (EntityTypeInterface $definition) {
return $definition->getConfigPrefix() . '.';
}, $this->definitions);

// Find all config, and then filter out entity configurations.
$names = $this->configStorage->listAll();
$names = array_combine($names, $names);
foreach ($names as $config_name) {
foreach ($config_prefixes as $config_prefix) {
if (str_starts_with($config_name, $config_prefix)) {
unset($names[$config_name]);
}
}
}

return $names;
}

/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state)
{
$selected = array_filter($form_state->getValue('configurations'));

// Remove keys and reset to sequential numeric keys
// to avoid, 'key contains a dot which is not supported' error.
$selected = array_values($selected);

$this->config('config_exporter.settings')
->set('selected_configurations', $selected)
->save();

parent::submitForm($form, $form_state);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace Drupal\config_exporter\Plugin\rest\resource;

use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
* Provides a resource for exporting configurations.
*
* @RestResource(
* id = "config_export_resource",
* label = @Translation("Config Export Resource"),
* uri_paths = {
* "canonical" = "/api/config-export/{config_name}"
* }
* )
*/
class ConfigExportResource extends ResourceBase {
/**
* The currently authenticated user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;

/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;

/**
* Constructs a ConfigExportResource instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The currently authenticated user.
* @param \Drupal\contact\MailHandlerInterface $mail_handler
* The contact mail handler service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, AccountProxyInterface $current_user, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->currentUser = $current_user;
$this->configFactory = $config_factory;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('current_user'),
$container->get('config.factory')
);
}

/**
* Responds to GET requests.
*
* Returns details for the specified configuration.
*
* @param string $config_name
* The name of the configuration.
*
* @return \Drupal\rest\ResourceResponse
* The response containing the configuration detail.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
* When configuration does not exists or not allowed to view.
*/
public function get($config_name = NULL) {

// Get the list of allowed configurations.
$config = $this->configFactory
->get('config_exporter.settings')
->get('selected_configurations') ?? [];

// When configuration is not allowed to view.
if (!in_array($config_name, $config)) {
throw new BadRequestHttpException(sprintf('Configuration (%s) yet not exposed to view.', $config_name));
}

$config = $this->configFactory->get($config_name);

// When config does not exists and only has an empty new config object.
if ($config->isNew()) {
throw new BadRequestHttpException(sprintf('Configuration (%s) does not exists.', $config_name));
}
else {
$data[$config_name] = $config->getRawData();

$response = new ResourceResponse($data);
$response->getCacheableMetadata()->addCacheTags(['config:config_exporter.settings']);
return $response;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Drupal\config_exporter\Plugin\rest\resource;

use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\Core\Config\ConfigFactoryInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
* Provides a resource for fetching allowed config list.
*
* @RestResource(
* id = "config_list",
* label = @Translation("Allowed Config List"),
* uri_paths = {
* "canonical" = "/api/config-list"
* }
* )
*/

class ConfigListResource extends ResourceBase {

/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;

/**
* Constructs a ConfigExportResource instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The currently authenticated user.
* @param \Drupal\contact\MailHandlerInterface $mail_handler
* The contact mail handler service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->configFactory = $config_factory;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('config.factory')
);
}

/**
* Responds to GET requests.
*
* Returns list of allowed configurations.
*
* @param string $config_name
* The name of the configuration.
*
* @return \Drupal\rest\ResourceResponse
* The response containing the list of allowed configurations.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
* When admin did not allowed any configuration to view.
*/
public function get() {

$config = $this->configFactory
->get('config_exporter.settings')
->get('selected_configurations') ?? [];

// When config does not exists and only has an empty new config object.
if (empty($config)) {
throw new BadRequestHttpException('Currently, site admin did not allow any configuration to view.');
}
else {
$response = new ResourceResponse($config);
$response->getCacheableMetadata()->addCacheTags(['config:config_exporter.settings']);
return $response;
}
}
}

0 comments on commit 8ea607e

Please sign in to comment.