Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

* [PR-564](https://github.com/itk-dev/deltag.aarhus.dk/pull/564)
Added hoeringsportal_anonymous_edit module
* [PR-557](https://github.com/itk-dev/deltag.aarhus.dk/pull/557)
* Change dialogue proposal backend
* Add seperate view for dialogue proposal comments
Expand Down
1 change: 1 addition & 0 deletions config/sync/core.extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module:
file_resup: 0
filter: 0
flag: 0
hoeringsportal_anonymous_edit: 0
hoeringsportal_audit_log: 0
hoeringsportal_citizen_proposal: 0
hoeringsportal_citizen_proposal_archiving: 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
uuid: 16550f09-0b5f-4b26-9763-f22c0b980ff4
langcode: da
status: true
dependencies:
module:
- hoeringsportal_anonymous_edit
id: hoeringsportal_anonymous_edit.content_recover
configuration:
email_body:
content:
value: "Gå til <a href=\"{{ recover_url }}\">{{ recover_url }}</a> for at finde dit eget indhold på deltag.aarhus.dk.\r\n"
format: email_html
email_subject:
value: 'Dit indhold på deltag.aarhus.dk'
91 changes: 91 additions & 0 deletions web/modules/custom/hoeringsportal_anonymous_edit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Anonymous edit

This module keeps track of anonymous user's content and allows them to update
and delete it.

The edit access is based on a long-lived cookie,
`hoeringsportal_anonymous_edit_token`, in a browser. If the user deletes the
cookie (or uses another browser), the cookie can be restored (or recovered?) via
a URL sent to the user's email address (see [Recovering the edit
token](#recovering-the-edit-token)).

When content, i.e. an instance of
[`EntityInterface`](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21EntityInterface.php/interface/EntityInterface/11.x),
is created by an anonymous user, the module dispatches an
`HoeringsportalAnonymousEditEvent` event to let other modules tell if the
content supports editing by anonymous users. It supported, the module ensures
that an edit token (cookie) is set in the users browser and the token is
attached to the content and any future content created by the user.

In addition to telling if the content is supported, the event subscriber can
also set an email address to be associated with the content. This email can
later be used to recover the edit token if need be.

See
[AnonymousEditSubscriber.php](../hoeringsportal_dialogue/src/EventSubscriber/AnonymousEditSubscriber.php)
for an example event subscriber implementation.

The module implements
[`hook_entity_access`](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/function/hook_entity_access/11.x)
to allow anonymous users to update and delete content matching the current edit
token.

## Content list

An anonymous user with a valid edtit token can find a list of its content on
`/hoeringsportal-anonymous-edit/content`. The content is grouped by content type
and bundle.

The content list uses the
[`hoeringsportal-anonymous-edit-content-index.html.twig`](templates/hoeringsportal-anonymous-edit-content-index.html.twig)
template file which can — and probably should be — overridden in the theme.

## Recovering the edit token

If the user looses its edit token, the token can be recovered if an email has
been attached to a piece of content created by the user.

The `hoeringsportal_anonymous_edit.content_request` route
(`/hoeringsportal-anonymous-edit/content/request`) lets the user enter an email
address and request an email with a link to recover the edit token.

The `hoeringsportal_anonymous_edit.content_recover` route
(`/hoeringsportal-anonymous-edit/content/recover/{token}`) asks the user to
confirm the email address, and if the email matches the token, the edit token is
restored and set as en edit token.

The recover email subject and body is managed on
`/admin/config/system/mailer/policy/hoeringsportal_anonymous_edit.content_recover`.
Twig can be used in both fields and the following variable is available (cf.
[src/Plugin/EmailBuilder/EmailBuilder.php](src/Plugin/EmailBuilder/EmailBuilder.php)):

| Name | Description |
|-------------|--------------------------|
| recover_url | The absolute recover URL |

## Development

By default only errors (and higher levels) are logged. During development the
module's log level can be set lower, e.g.

``` php
# settings.local.php
$settings['hoeringsportal_anonymous_edit']['log_level'] = \Drupal\Core\Logger\RfcLogLevel::DEBUG;
```

Show log messages with

``` shell
drush watchdog:show --type=hoeringsportal_anonymous_edit --extended
```

Peek in the database table:

``` shell
drush sql:query --extra=--table "SELECT * FROM hoeringsportal_anonymous_edit"
```

---

* [ ] What happens if the same email is attached to multiple tokens?
* [ ] What happens if a user has used multiple tokens?
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: "hoeringsportal_anonymous_edit"
type: module
description: "Allow anonymous users to edit their content"
package: Custom
core_version_requirement: ^10 || ^11

dependencies:
- drupal:symfony_mailer
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

/**
* @file
* Install functions for the hoeringsportal_anonymous_edit module.
*/

/**
* Implements hook_schema().
*/
function hoeringsportal_anonymous_edit_schema() {
$schema['hoeringsportal_anonymous_edit'] = [
'description' => 'Stores module data as key/value pairs per user.',
'fields' => [
'entity_type' => [
'description' => 'The entity type.',
'type' => 'varchar_ascii',
'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
'not null' => TRUE,
'default' => '',
],
'entity_bundle' => [
'description' => 'The entity bundle.',
'type' => 'varchar_ascii',
'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
'not null' => TRUE,
'default' => '',
],
'entity_uuid' => [
'description' => 'The entity UUID.',
'type' => 'varchar_ascii',
'length' => 36,
'not null' => TRUE,
'default' => '',
],
'owner_token' => [
'description' => 'The owner token (a UUID).',
'type' => 'varchar_ascii',
'length' => 36,
'not null' => TRUE,
'default' => '',
],
'owner_email' => [
'description' => 'The owner email.',
'type' => 'varchar_ascii',
'length' => 255,
'default' => '',
],
],
'primary key' => [
'entity_type',
'entity_bundle',
'entity_uuid',
],
'indexes' => [
'owner' => [
'owner_token',
'owner_email',
],
],
];

return $schema;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/**
* @file
* Functions for the hoeringsportal_anonymous_edit module.
*/

use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\hoeringsportal_anonymous_edit\Helper\Helper;

/**
* Implements hook_entity_insert().
*/
function hoeringsportal_anonymous_edit_entity_insert(EntityInterface $entity): void {
_hoeringsportal_anonymous_edit_get_helper()->entityInsert($entity);
}

/**
* Implements hook_entity_delete().
*/
function hoeringsportal_anonymous_edit_entity_delete(EntityInterface $entity): void {
_hoeringsportal_anonymous_edit_get_helper()->entityDelete($entity);
}

/**
* Implements hook_entity_access().
*/
function hoeringsportal_anonymous_edit_entity_access(EntityInterface $entity, $operation, AccountInterface $account): AccessResultInterface {
return _hoeringsportal_anonymous_edit_get_helper()->entityAccess($entity, $operation, $account);
}

/**
* Implements hook_theme().
*/
function hoeringsportal_anonymous_edit_theme($existing, $type, $theme, $path) : array {
return [
'hoeringsportal_anonymous_edit_content_index' => [
'variables' => [
'entities' => NULL,
],
],
];
}

/**
* Get the hoeringsportal_anonymous_edit helper.
*/
function _hoeringsportal_anonymous_edit_get_helper(): Helper {
return \Drupal::service(Helper::class);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
hoeringsportal_anonymous_edit.content:
path: "/hoeringsportal-anonymous-edit/content"
defaults:
_title: "Content"
_controller: '\Drupal\hoeringsportal_anonymous_edit\Controller\HoeringsportalAnonymousEditController'
options:
no_cache: "TRUE"
requirements:
_user_is_logged_in: "FALSE"

hoeringsportal_anonymous_edit.content_request:
path: "/hoeringsportal-anonymous-edit/content/request"
defaults:
_title: "Request"
_form: 'Drupal\hoeringsportal_anonymous_edit\Form\RequestForm'
requirements:
_user_is_logged_in: "FALSE"

hoeringsportal_anonymous_edit.content_recover:
path: "/hoeringsportal-anonymous-edit/content/recover/{token}"
defaults:
_title: "Recover"
_form: 'Drupal\hoeringsportal_anonymous_edit\Form\RecoverForm'
requirements:
_user_is_logged_in: "FALSE"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
_defaults:
autowire: true

hoeringsportal_anonymous_edit.logger:
parent: logger.channel_base
arguments: ["hoeringsportal_anonymous_edit"]

Drupal\hoeringsportal_anonymous_edit\Helper\ItemHelper:

Drupal\hoeringsportal_anonymous_edit\Helper\MailHelper:

Drupal\hoeringsportal_anonymous_edit\Helper\Helper:
tags:
- { name: event_subscriber }
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Drupal\hoeringsportal_anonymous_edit\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\hoeringsportal_anonymous_edit\Helper\Helper;

/**
* Returns responses for hoeringsportal_anonymous_edit routes.
*/
final class HoeringsportalAnonymousEditController extends ControllerBase {

public function __construct(
private readonly Helper $helper,
) {}

/**
* Action!
*/
public function __invoke(): array {
$entities = $this->helper->getContent();

$build['content'] = [
'#type' => 'theme',
'#theme' => 'hoeringsportal_anonymous_edit_content_index',
'#entities' => $entities,
];

return $build;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Drupal\hoeringsportal_anonymous_edit\Event;

use Drupal\Component\EventDispatcher\Event;
use Drupal\Core\Entity\EntityInterface;

/**
* Event for hoeringsportal_anonymous_edit.
*/
final class HoeringsportalAnonymousEditEvent extends Event {

/**
* Is the entity n this event supported?
*/
private bool $isSupported = FALSE;

/**
* An optional email for the entity.
*/
private ?string $email = NULL;

public function __construct(
private readonly EntityInterface $entity,
) {}

/**
* Get the entity.
*/
public function getEntity(): EntityInterface {
return $this->entity;
}

/**
* Is supported?
*/
public function isSupported(): bool {
return $this->isSupported;
}

/**
* Set is supported.
*/
public function setIsSupported(bool $isSupported = TRUE): self {
$this->isSupported = $isSupported;

return $this;
}

/**
* Get email.
*/
public function getEmail(): ?string {
return $this->email;
}

/**
* Set email.
*/
public function setEmail(string $email): self {
$this->email = $email;

return $this;
}

}
Loading