Skip to content

Commit

Permalink
Merge pull request #1853 from danskernesdigitalebibliotek/bnf-send
Browse files Browse the repository at this point in the history
Integrate BNF Article Import/Export Functionality. DDFHER-164
  • Loading branch information
rasben authored Dec 18, 2024
2 parents 124022f + becd376 commit b57362b
Show file tree
Hide file tree
Showing 22 changed files with 630 additions and 3 deletions.
5 changes: 3 additions & 2 deletions .lagoon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ tasks:
drush user:create patron --password="$PR_DRUPAL_PWD"
drush user:role:add 'patron' patron
drush user:create graphql_consumer --password="$PR_DRUPAL_PWD"
drush user:role:add 'graphql_consumer' graphql_consumer
drush user:create $GRAPHQL_USER_NAME --password="$GRAPHQL_USER_PASSWORD"
drush user:role:add 'external_system' $GRAPHQL_USER_NAME
drush user:role:add 'graphql_consumer' $GRAPHQL_USER_NAME
else
echo "Test users already exist. Skipping creation..."
fi
Expand Down
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ tasks:
- task dev:cli -- drush user:role:add 'patron' patron

- task dev:cli -- drush user:create graphql_consumer --password="test"
- task dev:cli -- drush user:role:add 'external_system' graphql_consumer
- task dev:cli -- drush user:role:add 'graphql_consumer' graphql_consumer

dev:import-profile-translations:
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ services:
VIRTUAL_PROTO: https
VIRTUAL_HOST: >-
${COMPOSE_PROJECT_NAME:-dapple-cms}.${DEV_TLD:-docker}
working_dir: /app

varnish:
labels:
- dev.orbstack.domains=${COMPOSE_PROJECT_NAME:-dapple-cms}.local
working_dir: /app
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ x-environment:
LAGOON_ENVIRONMENT: 'local'
LAGOON_ENVIRONMENT_TYPE: ${LAGOON_ENVIRONMENT_TYPE:-local}
WEBROOT: web
GRAPHQL_USER_NAME: graphql_consumer
GRAPHQL_USER_PASSWORD: test
BNF_SERVER_GRAPHQL_ENDPOINT: "https://dpl-cms.local/graphql"
# Uncomment if you like to have the system behave like in production
#LAGOON_ENVIRONMENT_TYPE: production

Expand Down
16 changes: 16 additions & 0 deletions docs/bnf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Bibliotekernes Nationale Formidling

Bibliotekernes Nationale Formidling (henceforth "BNF") is a national
team handling sharing of content for libraries. The `bnf_server` and
`bnf_client` modules support their work.

## Server module

The server module is enabled on the main BNF site, which acts as a hub
for content sharing. The BNF team uses this site to create and edit
content provided for the libraries.

## Client module

The client module is enabled on library sites and handles pushing and
fetching content to/from the BNF site.
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ parameters:
# more detailed typing of.
- '#.*\:\:(buildForm|getEditableConfigNames|submitForm|validateForm)\(\) .* no value type specified in iterable type array\.#'
- '#_alter\(\) has parameter \$form with no value type specified in iterable type array\.#'
- '#_form_submit\(\) has parameter \$form with no value type specified in iterable type array\.#'
# Drupal *_theme() implementation returns array which we cannot provide more detailed typing of.
- '#Function .*_theme\(\) return type has no value type specified in iterable type array\.#'
# Drupal *_theme() uses arrays which we cannot provide more detailed typing of.
Expand Down
5 changes: 5 additions & 0 deletions web/modules/custom/bnf/bnf.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: 'BNF Common'
type: module
description: 'Common code between BNF (Bibliotekernes Nationale Formidling) client and server modules'
package: 'DPL - BNF'
core_version_requirement: ^10
43 changes: 43 additions & 0 deletions web/modules/custom/bnf/bnf.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use Drupal\bnf\BnfStateEnum;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;

/**
* Implements hook_entity_base_field_info().
*
* Creating our custom programmatic fields.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* The field definitions.
*/
function bnf_entity_base_field_info(EntityTypeInterface $entity_type): array {
$fields = [];

// Create new fields for node bundle.
if ($entity_type->id() === 'node') {
$fields[BnfStateEnum::FIELD_NAME] = BaseFieldDefinition::create('list_integer')
->setName(BnfStateEnum::FIELD_NAME)
->setLabel(t('BNF State'))
->setDescription(t('The BNF state of the entity, defining if it was imported, exported, or neither.'))
->setSetting('allowed_values_function', 'bnf_get_bnf_state_allowed_values')
->setDefaultValue(BnfStateEnum::Undefined->value);
}

return $fields;
}

/**
* Provides allowed values for the BNF State field.
*
* @return string[]
* The enum values of BnfStateEnum.
*/
function bnf_get_bnf_state_allowed_values(): array {
$values = [];
foreach (BnfStateEnum::cases() as $case) {
$values[$case->value] = $case->name;
}
return $values;
}
12 changes: 12 additions & 0 deletions web/modules/custom/bnf/bnf.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
services:
bnf.importer:
class: Drupal\bnf\Services\BnfImporter
autowire: true
arguments:
$logger: '@logger.channel.bnf'

logger.channel.bnf:
parent: logger.channel_base
arguments:
- 'bnf'
7 changes: 7 additions & 0 deletions web/modules/custom/bnf/bnf_client/bnf_client.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: 'BNF Client'
type: module
description: 'BNF (Bibliotekernes Nationale Formidling) client, for sharing content with BNF'
package: 'DPL - BNF'
core_version_requirement: ^10
dependencies:
- bnf:bnf
89 changes: 89 additions & 0 deletions web/modules/custom/bnf/bnf_client/bnf_client.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

use Drupal\bnf\Exception\AlreadyExistsException;
use Drupal\bnf_client\Services\BnfExporter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\drupal_typed\DrupalTyped;

/**
* Implements hook_form_FORM_ID_alter().
*
* Altering the node form, and adding an option to export the node to BNF.
* If checked, a custom form submit handler will take care of the rest.
*
* @see bnf_client_form_node_form_submit()
*/
function bnf_client_form_node_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
$current_user = \Drupal::currentUser();

if (!$current_user->hasPermission('bnf client export nodes')) {
return;
}

if (empty($form['actions']['submit'])) {
\Drupal::logger('bnf_client')->error('Could not find submit button - cannot show BNF export flow.');
return;
}

// When adding the submit handler here, it happens after ::save() and any
// validation.
$form['actions']['submit']['#submit'][] = 'bnf_client_form_node_form_submit';

// Let's hide the publishing button when BNF is checked, as the value will
// be ignored and the node will be published regardless.
$form['status']['#states']['invisible'] =
[':input[name="bnf"]' => ['checked' => TRUE]];

$form['bnf'] = [
'#type' => 'checkbox',
'#title' => t('Publish and submit to BNF'),
'#description' => t('Please make sure that all content and media as part of this article is OK to be used by other libraries.'),
'#default_value' => FALSE,
];
}

/**
* A custom form submit handler, that publishes node, and "exports" to BNF.
*/
function bnf_client_form_node_form_submit(array $form, FormStateInterface $form_state): void {

if (empty($form_state->getValue('bnf'))) {
return;
}

try {
/** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
$form_object = $form_state->getFormObject();

/** @var \Drupal\node\NodeInterface $node */
$node = $form_object->getEntity();
$node->setPublished();
$node->save();
}
catch (\Exception $e) {
\Drupal::logger('bnf_client')->error('Could not publish node as part of BNF export. @message', ['@message' => $e->getMessage()]);

\Drupal::messenger()->addError(
t("Could not publish node - will not send to BNF.", [], ['context' => 'BNF'])
);
}

try {
$service = DrupalTyped::service(BnfExporter::class, 'bnf_client.exporter');
$service->exportNode($node);

\Drupal::messenger()->addStatus(
t("Content has been published and sent to BNF.", [], ['context' => 'BNF'])
);
}
catch (\Throwable $e) {
\Drupal::logger('bnf_client')->error('Could not export node to BNF. @message', ['@message' => $e->getMessage()]);

\Drupal::messenger()->addError(t('Could not export node to BNF.', [], ['context' => 'BNF']));

if ($e instanceof AlreadyExistsException) {
\Drupal::messenger()->addError(t('Node already has been exported to BNF.', [], ['context' => 'BNF']));
}

}
}
2 changes: 2 additions & 0 deletions web/modules/custom/bnf/bnf_client/bnf_client.permissions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bnf client export nodes:
title: 'Can export nodes to BNF'
7 changes: 7 additions & 0 deletions web/modules/custom/bnf/bnf_client/bnf_client.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
services:
bnf_client.exporter:
class: Drupal\bnf_client\Services\BnfExporter
autowire: true
arguments:
$logger: '@logger.channel.bnf'
112 changes: 112 additions & 0 deletions web/modules/custom/bnf/bnf_client/src/Services/BnfExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Drupal\bnf_client\Services;

use Drupal\bnf\BnfStateEnum;
use Drupal\bnf\Exception\AlreadyExistsException;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\node\NodeInterface;
use GuzzleHttp\ClientInterface;
use Psr\Log\LoggerInterface;
use function Safe\json_decode;
use function Safe\parse_url;

/**
* Service, related to exporting our content to BNF.
*
* We send an Import Request to BNF using GraphQL, along with information
* about the content and where they can access it using our GraphQL endpoint.
*/
class BnfExporter {

/**
* Constructor.
*/
public function __construct(
protected ClientInterface $httpClient,
protected UrlGeneratorInterface $urlGenerator,
protected TranslationInterface $translation,
protected LoggerInterface $logger,
) {}

/**
* Requesting BNF server to import the supplied node.
*/
public function exportNode(NodeInterface $node): void {
// generateFromRoute returns a string if we do not pass TRUE as the
// fourth argument.
/** @var string $callbackUrl */
$callbackUrl = $this->urlGenerator->generateFromRoute(
'graphql.query.graphql_compose_server',
[],
['absolute' => TRUE]
);

$uuid = $node->uuid();

$mutation = <<<GRAPHQL
mutation {
importRequest(uuid: "$uuid", callbackUrl: "$callbackUrl") {
status
message
}
}
GRAPHQL;

try {
$bnfServer = (string) getenv('BNF_SERVER_GRAPHQL_ENDPOINT');

if (!filter_var($bnfServer, FILTER_VALIDATE_URL)) {
throw new \InvalidArgumentException('The provided BNF server URL is not valid.');
}

$parsedUrl = parse_url($bnfServer);
$scheme = $parsedUrl['scheme'] ?? NULL;

if ($scheme !== 'https') {
throw new \InvalidArgumentException('The BNF server URL must use HTTPS.');
}

$response = $this->httpClient->request('post', $bnfServer, [
'headers' => [
'Content-Type' => 'application/json',
],
'auth' => [getenv('GRAPHQL_USER_NAME'), getenv('GRAPHQL_USER_PASSWORD')],
'json' => [
'query' => $mutation,
],
]);

$data = json_decode($response->getBody()->getContents(), TRUE);
}
catch (\Exception $e) {
$this->logger->error(
'Failed at exporting node to BNF server. @message',
['@message' => $e->getMessage()]);

throw new \Exception('Could not export node to BNF.');
}

$status = $data['data']['importRequest']['status'] ?? NULL;

if ($status !== 'success') {
$message = $data['data']['importRequest']['message'] ?? NULL;

$this->logger->error(
'Failed at exporting node to BNF server. @message',
['@message' => $message]);

if ($status === 'duplicate') {
throw new AlreadyExistsException();
}

throw new \Exception($message);
}

$node->set(BnfStateEnum::FIELD_NAME, BnfStateEnum::Exported->value);
$node->save();

}

}
7 changes: 7 additions & 0 deletions web/modules/custom/bnf/bnf_server/bnf_server.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: 'BNF Server'
type: module
description: 'BNF (Bibliotekernes Nationale Formidling) server'
package: 'DPL - BNF'
core_version_requirement: ^10
dependencies:
- bnf:bnf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Base file, necessary for GraphQL module to load our extension.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extend type Mutation {
importRequest(uuid: String!, callbackUrl: String!): ImportRequestResponse
}

type ImportRequestResponse {
status: String
message: String
}
Loading

0 comments on commit b57362b

Please sign in to comment.