diff --git a/web/modules/custom/bnf/bnf.module b/web/modules/custom/bnf/bnf.module index d19fd7ed8..11e83cce5 100644 --- a/web/modules/custom/bnf/bnf.module +++ b/web/modules/custom/bnf/bnf.module @@ -3,6 +3,7 @@ use Drupal\bnf\BnfStateEnum; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\node\NodeInterface; /** * Implements hook_entity_base_field_info(). @@ -41,3 +42,28 @@ function bnf_get_bnf_state_allowed_values(): array { } return $values; } + +/** + * Implements theme_preprocess_html(). + * + * Adding the node UUID as a metatag, that we can use when the user submits + * a URL to the BNF import form. + */ +function bnf_preprocess_html(array &$variables): void { + $route = \Drupal::routeMatch(); + $node = $route->getParameter('node'); + + if ($route->getRouteName() !== 'entity.node.canonical' || !($node instanceof NodeInterface)) { + return; + } + + $uuid_metatag = [ + '#tag' => 'meta', + '#attributes' => [ + 'name' => 'uuid', + 'content' => $node->uuid(), + ], + ]; + + $variables['page']['#attached']['html_head'][] = [$uuid_metatag, 'node-uuid']; +} diff --git a/web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml b/web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml new file mode 100644 index 000000000..5bce3976d --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/bnf_client.links.menu.yml @@ -0,0 +1,6 @@ +--- +bnf_client.import_content: + title: 'Import BNF content' + parent: system.admin_content + route_name: bnf_client.import_form + weight: 10 diff --git a/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml b/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml index e8a3704a5..b3a94242f 100644 --- a/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml +++ b/web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml @@ -1,2 +1,4 @@ bnf client export nodes: title: 'Can export nodes to BNF' +bnf client import nodes: + title: 'Can import nodes from BNF' diff --git a/web/modules/custom/bnf/bnf_client/bnf_client.routing.yml b/web/modules/custom/bnf/bnf_client/bnf_client.routing.yml new file mode 100644 index 000000000..6d3987e66 --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/bnf_client.routing.yml @@ -0,0 +1,16 @@ +--- +bnf_client.import_form: + path: '/admin/bnf/import' + defaults: + _form: '\Drupal\bnf_client\Form\BnfImportForm' + _title: 'Import' + requirements: + _permission: 'bnf client import nodes' + +bnf_client.import_confirm_form: + path: '/admin/bnf/import/{uuid}' + defaults: + _form: '\Drupal\bnf_client\Form\BnfImportConfirmForm' + _title: 'Confirm import' + requirements: + _permission: 'bnf client import nodes' diff --git a/web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php new file mode 100644 index 000000000..1a8130673 --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportConfirmForm.php @@ -0,0 +1,100 @@ +get('current_route_match'), + $container->get('bnf.importer') + ); + } + + /** + * {@inheritDoc} + */ + public function getFormId(): string { + return 'bnf_import_form_form'; + } + + /** + * {@inheritDoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $form['#title'] = $this->t('Confirm import of BNF content', [], ['context' => 'BNF']); + + $uuid = $this->routeMatch->getParameter('uuid'); + $bnfServer = (string) getenv('BNF_SERVER_GRAPHQL_ENDPOINT'); + + $form_state->set('uuid', $uuid); + $form_state->set('bnfServer', $bnfServer); + + $nodeData = $this->bnfImporter->loadNodeData($uuid, $bnfServer); + + $form['uuid'] = [ + '#title' => 'UUID', + '#type' => 'textfield', + '#default_value' => $uuid, + '#disabled' => TRUE, + ]; + + $form['label'] = [ + '#title' => $this->t('Content label', [], ['context' => 'BNF']), + '#type' => 'textfield', + '#default_value' => $nodeData['title'] ?? NULL, + '#disabled' => TRUE, + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Import content'), + ]; + + return $form; + } + + /** + * {@inheritDoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + + } + + /** + * {@inheritDoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $uuid = $form_state->get('uuid'); + $bnfServer = $form_state->get('bnfServer'); + $node = $this->bnfImporter->importNode($uuid, $bnfServer); + $form_state->setRedirect('entity.node.edit_form', ['node' => $node->id()]); + } + +} diff --git a/web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php new file mode 100644 index 000000000..fffb83073 --- /dev/null +++ b/web/modules/custom/bnf/bnf_client/src/Form/BnfImportForm.php @@ -0,0 +1,89 @@ +t('Import nodes from BNF', [], ['context' => 'BNF']); + $form['reference'] = [ + '#type' => 'textfield', + '#title' => $this->t('URL or UUID of content'), + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Preview content'), + ]; + + return $form; + } + + /** + * {@inheritDoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state): void { + $reference = $form_state->getValue('reference'); + $uuid = $this->parseAndValidateUuid($reference); + + if (empty($uuid)) { + $form_state->setErrorByName( + 'reference', + $this->t('Invalid URL or UUID.', [], ['context' => 'BNF']) + ); + } + + $form_state->set('uuid', $uuid); + } + + /** + * {@inheritDoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + $uuid = $form_state->get('uuid'); + $form_state->setRedirect('bnf_client.import_confirm_form', ['uuid' => $uuid]); + } + + /** + * Getting and validate a UUID from string or URL. + */ + protected function parseAndValidateUuid(string $reference): string|false { + // Detect if reference is a URL. + if (filter_var($reference, FILTER_VALIDATE_URL)) { + // Finding the metatag that contains the UUID. + $meta_tags = get_meta_tags($reference); + $reference = $meta_tags['uuid'] ?? NULL; + } + + return Uuid::isValid($reference) ? $reference : FALSE; + } + +} diff --git a/web/modules/custom/bnf/src/Services/BnfImporter.php b/web/modules/custom/bnf/src/Services/BnfImporter.php index 138db8519..dfcb4b516 100644 --- a/web/modules/custom/bnf/src/Services/BnfImporter.php +++ b/web/modules/custom/bnf/src/Services/BnfImporter.php @@ -6,10 +6,13 @@ use Drupal\bnf\Exception\AlreadyExistsException; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\node\NodeInterface; +use Drupal\paragraphs\ParagraphInterface; use GuzzleHttp\ClientInterface; use Psr\Log\LoggerInterface; use function Safe\json_decode; use function Safe\parse_url; +use function Safe\preg_replace; /** * Service related to importing content from an external source. @@ -20,6 +23,10 @@ */ class BnfImporter { + const ALLOWED_PARAGRAPHS = [ + 'text_body', + ]; + /** * Constructor. */ @@ -31,9 +38,129 @@ public function __construct( ) {} /** - * Importing a node from a GraphQL source endpoint. + * Building the query we use to get data from source. + */ + protected function getQuery(string $queryName, string $uuid): string { + // Example of GraphQL query: "nodeArticle". + return <<graphqlTypeToBundle($type); + + if (!in_array($bundleName, self::ALLOWED_PARAGRAPHS)) { + continue; + } + + $paragraph = ['type' => $bundleName]; + + // Map fields dynamically. + foreach ($paragraphData as $key => $value) { + if ($key === '__typename') { + continue; + } + + // Assume Drupal uses field names like "field_{key}". + $drupalFieldName = 'field_' . $key; + $paragraph[$drupalFieldName] = $value; + } + + $parsedParagraphs[] = $paragraph; + } + + return $parsedParagraphs; + } + + /** + * Creating the paragraphs, that we will add to the nodes. + * + * @param mixed[] $nodeData + * The GraphQL node data containing paragraphs. + * + * @return \Drupal\paragraphs\ParagraphInterface[] + * The paragraph entities. */ - public function importNode(string $uuid, string $endpointUrl, string $nodeType = 'article'): void { + protected function getParagraphs(array $nodeData): array { + $parsedParagraphs = $this->parseParagraphs($nodeData); + $storage = $this->entityTypeManager->getStorage('paragraph'); + $paragraphs = []; + foreach ($parsedParagraphs as $paragraphData) { + $paragraph = $storage->create($paragraphData); + + if ($paragraph instanceof ParagraphInterface) { + $paragraph->save(); + $paragraphs[] = $paragraph; + } + } + + return $paragraphs; + } + + /** + * Converts a GraphQL typename to a Drupal paragraph bundle name. + * + * @param string $typeName + * The GraphQL typename (e.g., ParagraphTextBody). + * + * @return string + * The Drupal paragraph bundle name (e.g., text_body). + */ + protected function graphqlTypeToBundle(string $typeName): string { + // Removing 'Paragraph' prefix. + $typeName = preg_replace('/^Paragraph/', '', $typeName); + + // Converting CamelCase to snake_case. + $pattern = '/(?<=\\w)(?=[A-Z])|(?<=[a-z])(?=[0-9])/'; + $typeName = preg_replace($pattern, '_', $typeName); + + return strtolower($typeName); + } + + /** + * Loading the node data from a GraphQL endpoint. + * + * @return mixed[] + * Array of node values, that we can use to create node entities. + */ + public function loadNodeData(string $uuid, string $endpointUrl, string $nodeType = 'article'): array { + $queryName = 'node' . ucfirst($nodeType); + $nodeStorage = $this->entityTypeManager->getStorage('node'); $existingNodes = @@ -48,18 +175,6 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = throw new AlreadyExistsException('Cannot import node - already exists.'); } - // Example of GraphQL query: "nodeArticle". - $queryName = 'node' . ucfirst($nodeType); - - // For now, we only support the title of the nodes. - $query = <<getQuery($queryName, $uuid); + $response = $this->httpClient->request('post', $endpointUrl, [ 'headers' => [ 'Content-Type' => 'application/json', @@ -92,10 +209,25 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = throw new \Exception('Could not retrieve content values.'); } + return $nodeData; + + } + + /** + * Importing a node from a GraphQL source endpoint. + */ + public function importNode(string $uuid, string $endpointUrl, string $nodeType = 'article'): NodeInterface { + $nodeStorage = $this->entityTypeManager->getStorage('node'); + try { + $nodeData = $this->loadNodeData($uuid, $endpointUrl, $nodeType); + $nodeData['type'] = $nodeType; $nodeData['uuid'] = $uuid; + $nodeData['status'] = NodeInterface::NOT_PUBLISHED; + $nodeData['field_paragraphs'] = $this->getParagraphs($nodeData); + /** @var \Drupal\node\NodeInterface $node */ $node = $nodeStorage->create($nodeData); $node->save(); @@ -116,6 +248,8 @@ public function importNode(string $uuid, string $endpointUrl, string $nodeType = '@type' => $nodeType, ]); + return $node; + } }