Skip to content
Snippets Groups Projects
Commit 5e841757 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3339609 by alexpott: How to configure the legal parts of the NewMessage API

parent bb2dd875
No related branches found
No related tags found
1 merge request!12Allow legal rights to be configured and entities to be opted out
Showing
with 270 additions and 23 deletions
......@@ -4,6 +4,11 @@ Integrates the [VG Wort service](https://tom.vgwort.de/portal/index) with Drupal
Install this module to add VG Wort's tracking 1x1 pixel to content entity types
of your choice.
## Excluding an entity from VG Wort
If you've configured nodes to have the VG Wort counter ID you can implement
`hook_vgwort_enable_for_entity()` to exclude specific entities from VG Wort.
See [vgwort.api.php](vgwort.api.php) for more information.
## Using an entity reference field to list participants
The module provides the ability to use any entity reference field to determine
VG Wort participant info. For example, if you have an author node type and
......
......@@ -12,3 +12,9 @@ queue_retry_time: 86400
entity_types:
node: [ ]
test_mode: false
legal_rights:
distribution: false
public_access: false
reproduction: false
declaration_of_granting: false
other_public_communication: false
......@@ -37,6 +37,25 @@ vgwort.settings:
test_mode:
label: 'Test mode'
type: boolean
legal_rights:
type: mapping
label: 'Legal rights'
mapping:
distribution:
label: 'Distribution right 17 UrhG)'
type: boolean
public_access:
label: 'Right of public access 19a UrhG)'
type: boolean
reproduction:
label: 'Reproduction Rights 16 UrhG)'
type: boolean
declaration_of_granting:
label: 'Declaration of Granting of Rights. The right of reproduction 16 UrhG), right of distribution 17 UrhG), right of public access 19a UrhG) and the declaration of granting rights must be confirmed.'
type: boolean
other_public_communication:
label: 'Other Public Communication Rights (§§ 19, 20, 21, 22 UrhG)'
type: boolean
field.widget.settings.vgwort_participant_info:
type: mapping
......
......@@ -113,14 +113,14 @@ class NewMessage implements \JsonSerializable {
* Publication location(s) where the text can be found.
* @param bool $distributionRight
* Distribution right (§ 17 UrhG).
* @param bool $otherRightsOfPublicReproduction
* Other Public Communication Rights (§§ 19, 20, 21, 22 UrhG).
* @param bool $publicAccessRight
* Right of public access (§ 19a UrhG).
* @param bool $reproductionRight
* Reproduction Rights (§ 16 UrhG).
* @param bool $rightsGrantedConfirmation
* Declaration of Granting of Rights.
* @param bool $otherRightsOfPublicReproduction
* Other Public Communication Rights (§§ 19, 20, 21, 22 UrhG).
* @param bool $withoutOwnParticipation
* Indication of whether the publisher is involved in the work.
*/
......@@ -130,10 +130,10 @@ class NewMessage implements \JsonSerializable {
array $participants,
array $webranges,
bool $distributionRight = FALSE,
bool $otherRightsOfPublicReproduction = FALSE,
bool $publicAccessRight = FALSE,
bool $reproductionRight = FALSE,
bool $rightsGrantedConfirmation = FALSE,
bool $otherRightsOfPublicReproduction = FALSE,
bool $withoutOwnParticipation = FALSE
) {
assert(Inspector::assertAllObjects($participants, Participant::class));
......
......@@ -7,6 +7,7 @@ use Drupal\advancedqueue\Job;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\vgwort\Plugin\AdvancedQueue\JobType\RegistrationNotification;
/**
......@@ -46,7 +47,7 @@ class EntityQueuer {
public function queueEntity(EntityInterface $entity): void {
// This service only supports entity types that can be configured in the UI.
// @see \Drupal\vgwort\Form\SettingsForm::buildForm()
if (!$entity instanceof EntityPublishedInterface) {
if (!$entity instanceof EntityPublishedInterface || !$entity instanceof FieldableEntityInterface) {
return;
}
......@@ -55,6 +56,11 @@ class EntityQueuer {
return;
}
// Only entities with a counter ID can be queued.
if ($entity->vgwort_counter_id->isEmpty()) {
return;
}
$queue = Queue::load('vgwort');
// If there is no queue fail silently. This ensures content can be inserted
// or updated prior to vgwort_post_update_create_queue() running.
......
......@@ -130,6 +130,44 @@ class SettingsForm extends ConfigFormBase {
'#description' => $this->t('Select entity types that can have a counter ID.'),
];
$legal_rights = $config->get('legal_rights');
$form['legal_rights'] = [
'#type' => 'details',
'#title' => $this->t('Legal rights'),
'#open' => TRUE,
'#tree' => TRUE,
];
$form['legal_rights']['distribution'] = [
'#type' => 'checkbox',
'#title' => $this->t('Distribution right (<a href="https://www.gesetze-im-internet.de/urhg/__17.html" target="_blank">§ 17 UrhG</a>)'),
'#default_value' => $legal_rights['distribution'],
];
$form['legal_rights']['public_access'] = [
'#type' => 'checkbox',
'#title' => $this->t('Right of public access (<a href="https://www.gesetze-im-internet.de/urhg/__19a.html" target="_blank">§ 19a UrhG</a>)'),
'#default_value' => $legal_rights['public_access'],
];
$form['legal_rights']['reproduction'] = [
'#type' => 'checkbox',
'#title' => $this->t('Reproduction Rights (<a href="https://www.gesetze-im-internet.de/urhg/__16.html" target="_blank">§ 16 UrhG</a>)'),
'#default_value' => $legal_rights['reproduction'],
];
$form['legal_rights']['declaration_of_granting'] = [
'#type' => 'checkbox',
'#title' => $this->t('Declaration of Granting of Rights.'),
'#default_value' => $legal_rights['declaration_of_granting'],
];
$form['legal_rights']['other_public_communication'] = [
'#type' => 'checkbox',
'#title' => $this->t('Other Public Communication Rights (§§ <a href="https://www.gesetze-im-internet.de/urhg/__19.html" target="_blank">19</a>, <a href="https://www.gesetze-im-internet.de/urhg/__20.html" target="_blank">20</a>, <a href="https://www.gesetze-im-internet.de/urhg/__21.html" target="_blank">21</a>, <a href="https://www.gesetze-im-internet.de/urhg/__22.html" target="_blank">22</a> UrhG)'),
'#default_value' => $legal_rights['other_public_communication'],
];
// Ensure the expected legal rights are granted as not doing so will cause
// errors on VG Wort.
if (empty($form_state->getUserInput()) && ($legal_rights['distribution'] !== TRUE || $legal_rights['public_access'] !== TRUE || $legal_rights['reproduction'] !== TRUE || $legal_rights['declaration_of_granting'] !== TRUE)) {
$this->messenger()->addWarning($this->t('The right of reproduction (§ 16 UrhG), right of distribution (§ 17 UrhG), right of public access (§ 19a UrhG) and the declaration of granting rights must be confirmed.'));
}
return parent::buildForm($form, $form_state);
}
......@@ -137,6 +175,8 @@ class SettingsForm extends ConfigFormBase {
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// NOTE: Do not set a form error here or setting a password on this form
// will have a very bad UX.
if ($form_state->getValue('delete_password')) {
$form_state->setValue('password', '');
}
......@@ -165,6 +205,7 @@ class SettingsForm extends ConfigFormBase {
->set('publisher_id', $form_state->getValue('publisher_id'))
->set('image_domain', $form_state->getValue('image_domain'))
->set('entity_types', $entity_types)
->set('legal_rights', $form_state->getValue('legal_rights'))
->save();
parent::submitForm($form, $form_state);
......
......@@ -2,6 +2,7 @@
namespace Drupal\vgwort;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Render\RendererInterface;
......@@ -27,7 +28,12 @@ class MessageGenerator {
/**
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
protected $renderer;
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* Constructs a MessageGenerator object.
......@@ -38,11 +44,14 @@ class MessageGenerator {
* The entity type manager service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
*/
public function __construct(ParticipantListManager $participantListManager, EntityTypeManagerInterface $entityTypeManager, RendererInterface $renderer) {
public function __construct(ParticipantListManager $participantListManager, EntityTypeManagerInterface $entityTypeManager, RendererInterface $renderer, ConfigFactoryInterface $configFactory) {
$this->participantListManager = $participantListManager;
$this->entityTypeManager = $entityTypeManager;
$this->renderer = $renderer;
$this->config = $configFactory->get('vgwort.settings');
}
/**
......@@ -60,7 +69,7 @@ class MessageGenerator {
* Throw if the entity does not have a vgwort_counter_id field.
*/
public function entityToNewMessage(FieldableEntityInterface $entity, string $view_mode = 'full'): NewMessage {
if (!$entity->hasField('vgwort_counter_id')) {
if (!$entity->hasField('vgwort_counter_id') || $entity->vgwort_counter_id->isEmpty()) {
throw new \RuntimeException('Entities must have the vgwort_counter_id in order to generate a VG Wort new massage notification');
}
$vgwort_id = $entity->vgwort_counter_id->value;
......@@ -78,12 +87,19 @@ class MessageGenerator {
// decoupled.
$webranges = [new Webrange([$entity->toUrl()->setAbsolute()->toString()])];
// @todo How to set the flags correctly.
$legal_rights = $this->config->get('legal_rights');
// @todo How to set other_public_communication and withoutOwnParticipation
// correctly.
return new NewMessage(
$vgwort_id,
$text,
$participants,
$webranges
$webranges,
$legal_rights['distribution'],
$legal_rights['public_access'],
$legal_rights['reproduction'],
$legal_rights['declaration_of_granting'],
$legal_rights['other_public_communication'],
);
}
......
......@@ -104,6 +104,11 @@ class RegistrationNotification extends JobTypeBase implements ContainerFactoryPl
return JobResult::failure(sprintf('The entity %s:%s does not exist', $entity_type, $entity_id));
}
// Only entities with a counter ID can be process.
if ($entity->vgwort_counter_id->isEmpty()) {
return JobResult::failure(sprintf('The entity %s:%s does not have a VG Wort counter ID', $entity_type, $entity_id));
}
// @todo allow view mode to be configured. In payload? Per entity type? In
// config?
$message = $this->messageGenerator->entityToNewMessage($entity);
......
......@@ -28,6 +28,21 @@ class CounterIdFieldItemList extends FieldItemList {
return;
}
$enabled_for_entity = TRUE;
\Drupal::moduleHandler()->invokeAllWith('vgwort_enable_for_entity', function (callable $hook) use ($entity, &$enabled_for_entity) {
// Once an implementation has returned false do not call any other
// implementation.
if ($enabled_for_entity) {
$enabled_for_entity = $hook($entity);
}
});
if (!$enabled_for_entity) {
// An implementation og hook_vgwort_enable_for_entity() has returned
// false.
return;
}
// @todo Do we need to use base64 because the UUID contains hyphens?
// Example: vgzm.970-123456789
$value = "$prefix.$publisher_id-{$entity->uuid()}";
......
......@@ -50,4 +50,12 @@ class CounterId extends FieldItemBase {
return [];
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
// We need to override this because everything is computed.
return $this->properties['value']->getValue() === NULL;
}
}
......@@ -82,10 +82,20 @@ class VgWort extends DataProducerPluginBase implements ContainerFactoryPluginInt
* The VG Wort field data.
*/
public function resolve(ContentEntityInterface $entity): ?array {
if ($entity->vgwort_counter_id === NULL) {
// The computed field is not added to this entity type.
if (!$entity->hasField('vgwort_counter_id')) {
return NULL;
}
// VG Wort is not set up or hook_vgwort_enable_for_entity() has disabled it.
if ($entity->vgwort_counter_id->isEmpty()) {
return [
'counterId' => '',
'url' => '',
'rendered' => '',
];
}
$render_array = CounterIdImage::getRenderArray($entity->vgwort_counter_id->url, $this->configFactory->get('vgwort.settings'));
return [
'counterId' => $entity->vgwort_counter_id->value,
......
name: 'VG Wort test'
type: module
description: 'Support module for VG Wort testing.'
package: Testing
<?php
/**
* @file
* Test functionality.
*/
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_vgwort_enable_for_entity().
*/
function vgwort_test_vgwort_enable_for_entity(EntityInterface $entity): bool {
return !in_array($entity->id(), \Drupal::state()->get('vgwort_test_vgwort_enable_for_entity', []), TRUE);
}
......@@ -28,6 +28,11 @@ class VGWortTest extends BrowserTestBase {
*/
private const TEST_MODE_MESSAGE = 'The test mode is enabled. The 1x1 pixel will be added as HTML comment to the selected entity_types.';
/**
* @see \Drupal\vgwort\Form\SettingsForm::buildForm()
*/
public const LEGAL_MESSAGE = 'The right of reproduction (§ 16 UrhG), right of distribution (§ 17 UrhG), right of public access (§ 19a UrhG) and the declaration of granting rights must be confirmed.';
/**
* {@inheritdoc}
*/
......@@ -49,6 +54,7 @@ class VGWortTest extends BrowserTestBase {
// Module settings.
$this->drupalGet('admin/config');
$this->clickLink('VG Wort settings');
$this->assertSession()->pageTextContains(self::LEGAL_MESSAGE);
$this->assertSession()->pageTextNotContains(self::TEST_MODE_MESSAGE);
$this->assertSession()->fieldValueEquals('username', '');
$this->assertSession()->fieldValueEquals('password', '');
......@@ -67,6 +73,15 @@ class VGWortTest extends BrowserTestBase {
$this->assertSession()->fieldExists('entity_types[taxonomy_term]')->check();
$this->submitForm([], 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
// The legal fields cause a message to be set.
$this->assertSession()->pageTextContains(self::LEGAL_MESSAGE);
$this->assertSession()->fieldExists('legal_rights[distribution]')->check();
$this->assertSession()->fieldExists('legal_rights[public_access]')->check();
$this->assertSession()->fieldExists('legal_rights[reproduction]')->check();
$this->assertSession()->fieldExists('legal_rights[declaration_of_granting]')->check();
$this->submitForm([], 'Save configuration');
$this->assertSession()->pageTextNotContains(self::LEGAL_MESSAGE);
$this->assertSession()->pageTextContains('The configuration options have been saved.');
$this->assertSession()->fieldValueEquals('username', 'aaaBBB');
$this->assertSession()->fieldValueEquals('password', '');
$this->assertSame('t3st', $this->config('vgwort.settings')->get('password'));
......
......@@ -51,6 +51,22 @@ class VGWortUpdateTest extends UpdatePathTestBase {
$this->assertInstanceOf(Queue::class, $queue);
$this->assertSame('VG Wort', $queue->label());
$this->assertTrue(\Drupal::database()->schema()->tableExists(EntityJobMapper::TABLE));
$expected_legal_rights = [
'distribution' => FALSE,
'public_access' => FALSE,
'reproduction' => FALSE,
'declaration_of_granting' => FALSE,
'other_public_communication' => FALSE,
];
$this->assertSame($expected_legal_rights, $this->config('vgwort.settings')->get('legal_rights'));
$this->assertSession()->linkExists('Visit the VG Wort settings form');
$links = $this->getSession()->getPage()->findAll('named', ['link', 'Visit the VG Wort settings form']);
$url = $links[0]->getAttribute('href');
// Login as we run the test using update free access in settings.php.
$this->drupalLogin($this->drupalCreateUser([], NULL, TRUE));
$this->drupalGet($url);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains(VGWortTest::LEGAL_MESSAGE);
}
}
......@@ -63,9 +63,9 @@ class VgWortDataProducerTest extends GraphQLTestBase {
]);
$this->assertNotNull($result);
$this->assertEquals($node->vgwort_counter_id->value, $result['counterId']);
$this->assertEquals($node->vgwort_counter_id->url, $result['url']);
$this->assertEquals('<img src="\\\\' . $node->vgwort_counter_id->url . '" height="1" width="1" alt=""/>', $result['rendered']);
$this->assertSame($node->vgwort_counter_id->value, $result['counterId']);
$this->assertSame($node->vgwort_counter_id->url, $result['url']);
$this->assertSame('<img src="\\\\' . $node->vgwort_counter_id->url . '" height="1" width="1" alt=""/>', (string) $result['rendered']);
// Test the test mode.
$this->config('vgwort.settings')
......@@ -75,9 +75,9 @@ class VgWortDataProducerTest extends GraphQLTestBase {
'entity' => $node,
]);
$this->assertNotNull($result);
$this->assertEquals($node->vgwort_counter_id->value, $result['counterId']);
$this->assertEquals($node->vgwort_counter_id->url, $result['url']);
$this->assertEquals('<!-- <img src="\\\\' . $node->vgwort_counter_id->url . '" height="1" width="1" alt=""/> -->', $result['rendered']);
$this->assertSame($node->vgwort_counter_id->value, $result['counterId']);
$this->assertSame($node->vgwort_counter_id->url, $result['url']);
$this->assertSame('<!-- <img src="\\\\' . $node->vgwort_counter_id->url . '" height="1" width="1" alt=""/> -->', (string) $result['rendered']);
$term = Term::create([
'vid' => 'tags',
......@@ -102,9 +102,23 @@ class VgWortDataProducerTest extends GraphQLTestBase {
'entity' => $term,
]);
$this->assertNotNull($result);
$this->assertEquals($term->vgwort_counter_id->value, $result['counterId']);
$this->assertEquals($term->vgwort_counter_id->url, $result['url']);
$this->assertEquals('<!-- <img src="\\\\' . $term->vgwort_counter_id->url . '" height="1" width="1" alt=""/> -->', $result['rendered']);
$this->assertSame($term->vgwort_counter_id->value, $result['counterId']);
$this->assertSame($term->vgwort_counter_id->url, $result['url']);
$this->assertSame('<!-- <img src="\\\\' . $term->vgwort_counter_id->url . '" height="1" width="1" alt=""/> -->', (string) $result['rendered']);
// Ensure the data producer respects hook_vgwort_enable_for_entity().
$this->enableModules(['vgwort_test']);
$this->container->get('entity_type.manager')->getStorage('taxonomy_term')->resetCache();
$this->container->get('state')->set('vgwort_test_vgwort_enable_for_entity', [$term->id()]);
$term = Term::load($term->id());
$result = $this->executeDataProducer('vgwort', [
'entity' => $term,
]);
$this->assertNotNull($result);
$this->assertSame('', $result['counterId']);
$this->assertSame('', $result['url']);
$this->assertSame('', $result['rendered']);
}
}
......@@ -54,7 +54,7 @@ class MessageGeneratorTest extends VgWortKernelTestBase {
$expected_message = <<<JSON
{
"distributionRight": false,
"distributionRight": true,
"messagetext": {
"lyric": false,
"shorttext": "A title",
......@@ -72,9 +72,9 @@ class MessageGeneratorTest extends VgWortKernelTestBase {
}
],
"privateidentificationid": "vgzm.123456-{$entity->uuid()}",
"publicAccessRight": false,
"reproductionRight": false,
"rightsGrantedConfirmation": false,
"publicAccessRight": true,
"reproductionRight": true,
"rightsGrantedConfirmation": true,
"webranges": [
{
"urls": [
......
......@@ -106,6 +106,17 @@ class RegistrationNotificationJobTypeTest extends VgWortKernelTestBase {
$this->assertEmpty($this->history);
}
public function testEntityWithoutCounterId() {
$this->enableModules(['vgwort_test']);
$this->container->get('state')->set('vgwort_test_vgwort_enable_for_entity', [$this->entity->id()]);
$this->container->get('entity_type.manager')->getStorage('entity_test')->resetCache();
$job = RegistrationNotification::createJob($this->entity);
$result = $this->processJob($job);
$this->assertSame(Job::STATE_FAILURE, $result->getState());
$this->assertSame('The entity entity_test:1 does not have a VG Wort counter ID', (string) $result->getMessage());
$this->assertEmpty($this->history);
}
public function testMissingCredentials() {
$this->handler->append(new Response(401, ['Content-Type' => ['text/html', 'charset=UTF-8']], '<html><head><title>Error</title></head><body>Unauthorized</body></html>'));
......
......@@ -115,4 +115,38 @@ class VgWortKernelTest extends VgWortKernelTestBase {
$this->assertSame('vgwort_test.0.surname', $violations->get(0)->getPropertyPath());
}
public function testVgwortEnableForEntityHook() {
$entity_storage = $this->container->get('entity_type.manager')->getStorage(static::ENTITY_TYPE);
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $entity_storage->create([
'text' => 'Some text',
'name' => 'A title',
]);
$entity->save();
$another_entity = $entity_storage->create([
'text' => 'Another text',
'name' => 'Anoter title',
]);
$another_entity->save();
$this->assertSame('vgzm.123456-' . $entity->uuid(), $entity->vgwort_counter_id->value);
$this->assertSame('vgzm.123456-' . $another_entity->uuid(), $another_entity->vgwort_counter_id->value);
$this->enableModules(['vgwort_test']);
$this->container->get('state')->set('vgwort_test_vgwort_enable_for_entity', [$another_entity->id()]);
$entity_storage->resetCache();
$entity = $entity_storage->load($entity->id());
$another_entity = $entity_storage->load($another_entity->id());
$this->assertSame('vgzm.123456-' . $entity->uuid(), $entity->vgwort_counter_id->value);
$this->assertNull($another_entity->vgwort_counter_id->value);
$this->container->get('state')->set('vgwort_test_vgwort_enable_for_entity', [$entity->id()]);
$entity_storage->resetCache();
$entity = $entity_storage->load($entity->id());
$another_entity = $entity_storage->load($another_entity->id());
$this->assertNull($entity->vgwort_counter_id->value);
$this->assertSame('vgzm.123456-' . $another_entity->uuid(), $another_entity->vgwort_counter_id->value);
}
}
......@@ -24,6 +24,13 @@ trait KernelSetupTrait {
->set('publisher_id', 123456)
->set('image_domain', 'http://example.com')
->set('entity_types', $entity_types)
->set('legal_rights', [
'distribution' => TRUE,
'public_access' => TRUE,
'reproduction' => TRUE,
'declaration_of_granting' => TRUE,
'other_public_communication' => FALSE,
])
->save();
if (array_key_exists('ENTITY_TYPE', (new \ReflectionClass($this))->getConstants())) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment