Commit 057d0060 authored by jrockowitz's avatar jrockowitz Committed by jrockowitz

Issue #2854020 by jrockowitz: Provide a mechanism to log submission transactions

parent ca442bbc
......@@ -687,6 +687,11 @@ webform.handler.*:
type: mapping
label: 'Handler settings'
webform.handler.log:
type: mapping
label: 'Log'
mapping: { }
webform.handler.email:
type: mapping
label: 'Email'
......
<?php
namespace Drupal\webform_node\Controller;
use Drupal\Core\Entity\EntityInterface;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionInterface;
use Drupal\webform\Controller\WebformSubmissionLogController;
/**
* Returns responses for webform submission log routes.
*/
class WebformNodeSubmissionLogController extends WebformSubmissionLogController {
/**
* Wrapper that allows the $node to be used as $source_entity.
*/
public function overview(WebformInterface $webform = NULL, WebformSubmissionInterface $webform_submission = NULL, EntityInterface $node = NULL) {
return parent::overview($webform, $webform_submission, $node);
}
}
......@@ -38,12 +38,7 @@ class WebformNodeAccessTest extends WebformNodeTestBase {
global $base_path;
// Create webform node that references the contact webform.
$webform_id = 'contact';
$webform = Webform::load($webform_id);
$node = $this->drupalCreateNode(['type' => 'webform']);
$node->webform->target_id = $webform_id;
$node->webform->status = WebformInterface::STATUS_OPEN;
$node->save();
$node = $this->createWebformNode('contact');
$nid = $node->id();
/**************************************************************************/
......@@ -113,12 +108,8 @@ class WebformNodeAccessTest extends WebformNodeTestBase {
* @see \Drupal\webform\Tests\WebformAccessTest::testAccessRules
*/
public function testAccessRules() {
// Create webform node that references the contact webform.
$webform = Webform::load('contact');
$node = $this->drupalCreateNode(['type' => 'webform']);
$node->webform->target_id = 'contact';
$node->webform->status = WebformInterface::STATUS_OPEN;
$node->save();
$node = $this->createWebformNode('contact');
$nid = $node->id();
// Log in normal user and get their rid.
......
<?php
namespace Drupal\webform_node\Tests;
use Drupal\Core\Url;
use Drupal\webform\Entity\Webform;
use Drupal\webform\WebformInterface;
/**
* Tests for webform node submission log.
*
* @group WebformNode
*/
class WebformNodeSubmissionLogTest extends WebformNodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'webform', 'webform_node'];
/**
* Webforms to load.
*
* @var array
*/
protected static $testWebforms = ['test_handler_log'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create users.
$this->createUsers();
}
/**
* Tests webform submission log.
*/
public function testSubmissionLog() {
global $base_path;
$node = $this->createWebformNode('test_handler_log');
$nid = $node->id();
$sid = $this->postNodeSubmission($node);
$log = $this->getLastSubmissionLog();
$this->assertEqual($log->lid, 1);
$this->assertEqual($log->sid, 1);
$this->assertEqual($log->uid, 0);
$this->assertEqual($log->handler_id, 'test_log');
$this->assertEqual($log->operation, 'submission created');
$this->assertEqual($log->message, t('@title: Submission #1 created.', ['@title' => $node->label()]));
$this->assertEqual($log->webform_id, 'test_handler_log');
$this->assertEqual($log->entity_type, 'node');
$this->assertEqual($log->entity_id, $node->id());
// Login.
$this->drupalLogin($this->adminWebformUser);
// Check webform node results log table has record.
$this->drupalGet("node/$nid/webform/results/log");
$this->assertResponse(200);
$this->assertNoRaw('No log messages available.');
$this->assertRaw('<a href="' . $base_path . 'node/' . $nid . '/webform/submission/' . $sid . '/log">' . $sid . '</a>');
$this->assertRaw(t('@title: Submission #1 created.', ['@title' => $node->label()]));
// Check webform node submission log tab.
$this->drupalGet("node/$nid/webform/submission/$sid/log");
$this->assertResponse(200);
}
}
......@@ -42,19 +42,13 @@ class WebformNodeTest extends WebformNodeTestBase {
* Tests webform node.
*/
public function testNode() {
// Create node.
$node = $this->drupalCreateNode(['type' => 'webform']);
$node = $this->createWebformNode('contact');
/**************************************************************************/
// Webform node basic.
/**************************************************************************/
// Check contact webform.
$node->webform->target_id = 'contact';
$node->webform->status = WebformInterface::STATUS_OPEN;
$node->webform->open = '';
$node->webform->close = '';
$node->save();
$this->drupalGet('node/' . $node->id());
$this->assertRaw('webform-submission-contact-form');
$this->assertNoFieldByName('name', 'John Smith');
......
......@@ -6,6 +6,7 @@ use Drupal\node\NodeInterface;
use Drupal\webform\Entity\Webform;
use Drupal\webform\Tests\WebformTestBase;
use Drupal\webform\Plugin\Field\FieldType\WebformEntityReferenceItem;
use Drupal\webform\WebformInterface;
/**
* Base tests for webform node.
......@@ -43,4 +44,23 @@ abstract class WebformNodeTestBase extends WebformTestBase {
return $this->getLastSubmissionId($webform);
}
/**
* Create a webform node.
*
* @param string $webform_id
* A webform id.
*
* @return \Drupal\node\NodeInterface
* A webform node.
*/
protected function createWebformNode($webform_id) {
$node = $this->drupalCreateNode(['type' => 'webform']);
$node->webform->target_id = $webform_id;
$node->webform->status = WebformInterface::STATUS_OPEN;
$node->webform->open = '';
$node->webform->close = '';
$node->save();
return $node;
}
}
......@@ -29,6 +29,11 @@ entity.node.webform.results_clear:
route_name: entity.node.webform.results_clear
parent_id: entity.node.webform.results
entity.node.webform.results_log:
title: 'Log'
route_name: entity.node.webform.results_log
parent_id: entity.node.webform.results
# Submission
entity.node.webform_submission.canonical:
......@@ -86,6 +91,11 @@ entity.node.webform_submission.delete_form:
route_name: entity.node.webform_submission.delete_form
base_route: entity.node.webform_submission.canonical
entity.node.webform_submission.log:
title: 'Log'
route_name: entity.node.webform_submission.log
base_route: entity.node.webform_submission.canonical
# User Submission
entity.node.webform.user.submission:
......
......@@ -167,6 +167,22 @@ entity.node.webform.results_clear:
requirements:
_custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformResultsAccess'
entity.node.webform.results_log:
path: '/node/{node}/webform/results/log'
defaults:
_controller: '\Drupal\webform_node\Controller\WebformNodeSubmissionLogController::overview'
_title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
operation: webform_submission_view
entity_access: 'webform.submission_view_any'
options:
_admin_route: TRUE
parameters:
node:
type: 'entity:node'
requirements:
_permission: 'access webform submission log'
_custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformResultsAccess'
entity.node.webform_submission.canonical:
path: '/node/{node}/webform/submission/{webform_submission}'
defaults:
......@@ -323,3 +339,19 @@ entity.node.webform_submission.delete_form:
type: 'entity:node'
requirements:
_custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformSubmissionAccess'
entity.node.webform_submission.log:
path: '/node/{node}/webform/submission/{webform_submission}/log'
defaults:
_controller: '\Drupal\webform\Controller\WebformSubmissionLogController::overview'
_title_callback: 'Drupal\webform\Controller\WebformSubmissionController::title'
operation: webform_submission_view
entity_access: 'webform_submission.view_any'
options:
_admin_route: TRUE
parameters:
node:
type: 'entity:node'
requirements:
_permission: 'access webform submission log'
_custom_access: '\Drupal\webform_node\Access\WebformNodeAccess::checkWebformSubmissionAccess'
<?php
namespace Drupal\webform\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformRequestInterface;
use Drupal\webform\WebformSubmissionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for webform submission log routes.
*
* Copied from: \Drupal\dblog\Controller\DbLogController
*/
class WebformSubmissionLogController extends ControllerBase {
/**
* The database service.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The webform storage.
*
* @var \Drupal\webform\WebformStorageInterface
*/
protected $webformStorage;
/**
* The webform submission storage.
*
* @var \Drupal\webform\WebformSubmissionStorageInterface
*/
protected $webformSubmissionStorage;
/**
* Webform request handler.
*
* @var \Drupal\webform\WebformRequestInterface
*/
protected $requestHandler;
/**
* Constructs a DbLogController object.
*
* @param \Drupal\Core\Database\Connection $database
* A database connection.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\webform\WebformRequestInterface $request_handler
* The webform request handler.
*/
public function __construct(Connection $database, DateFormatterInterface $date_formatter, WebformRequestInterface $request_handler) {
$this->database = $database;
$this->dateFormatter = $date_formatter;
$this->webformStorage = $this->entityTypeManager()->getStorage('webform');
$this->webformSubmissionStorage = $this->entityTypeManager()->getStorage('webform_submission');
$this->userStorage = $this->entityTypeManager()->getStorage('user');
$this->requestHandler = $request_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('date.formatter'),
$container->get('webform.request')
);
}
/**
* Displays a listing of webform submission log messages.
*
* @param \Drupal\webform\WebformInterface|NULL $webform
* A webform.
* @param \Drupal\webform\WebformInterface|NULL $webform_submission
* A webform submission.
* @param \Drupal\Core\Entity\EntityInterface|NULL $source_entity
* A source entity.
*
* @param null $sid
*
* @return array
* A render array as expected by drupal_render().
*/
public function overview(WebformInterface $webform = NULL, WebformSubmissionInterface $webform_submission = NULL, EntityInterface $source_entity = NULL) {
if (empty($webform) && !empty($webform_submission)) {
$webform = $webform_submission->getWebform();
}
if (empty($source_entity) && !empty($webform_submission)) {
$source_entity = $webform_submission->getSourceEntity();
}
// Header.
$header = [];
$header['lid'] = ['data' => $this->t('#'), 'field' => 'l.lid', 'sort' => 'desc'];
if (empty($webform)) {
$header['webform_id'] = ['data' => $this->t('Webform'), 'field' => 'ws.webform_id', 'class' => [RESPONSIVE_PRIORITY_MEDIUM]];
}
if (empty($source_entity)) {
$header['entity'] = ['data' => $this->t('Submitted to'), 'class' => [RESPONSIVE_PRIORITY_LOW]];
}
if (empty($webform_submission)) {
$header['sid'] = ['data' => $this->t('Submission'), 'field' => 'l.sid'];
}
$header['handler_id'] = ['data' => $this->t('Handler'), 'field' => 'l.handler_id'];
$header['operation'] = ['data' => $this->t('Operation'), 'field' => 'l.operation', 'class' => [RESPONSIVE_PRIORITY_MEDIUM]];
$header['message'] = ['data' => $this->t('Message'), 'field' => 'l.message', 'class' => [RESPONSIVE_PRIORITY_LOW]];
$header['uid'] = ['data' => $this->t('User'), 'field' => 'ufd.name', 'class' => [RESPONSIVE_PRIORITY_LOW]];
$header['timestamp'] = ['data' => $this->t('Date'), 'field' => 'l.timestamp', 'sort' => 'desc', 'class' => [RESPONSIVE_PRIORITY_LOW]];
// Query.
$query = $this->database->select('webform_submission_log', 'l')
->extend('\Drupal\Core\Database\Query\PagerSelectExtender')
->extend('\Drupal\Core\Database\Query\TableSortExtender');
$query->leftJoin('users_field_data', 'ufd', 'l.uid = ufd.uid');
$query->leftJoin('webform_submission', 'ws', 'l.sid = ws.sid');
$query->fields('l', [
'lid',
'uid',
'sid',
'handler_id',
'operation',
'message',
'timestamp',
]);
$query->fields('ws', [
'webform_id',
'entity_type',
'entity_id',
]);
if ($webform) {
$query->condition('ws.webform_id', $webform->id());
}
if ($webform_submission) {
$query->condition('l.sid', $webform_submission->id());
}
if ($source_entity) {
$query->condition('ws.entity_type', $source_entity->getEntityTypeId());
$query->condition('ws.entity_id', $source_entity->id());
}
$result = $query
->limit(50)
->orderByHeader($header)
->execute();
// Rows.
$rows = [];
foreach ($result as $log) {
$row = [];
$row['lid'] = $log->lid;
if (empty($webform)) {
$log_webform = $this->webformStorage->load($log->webform_id);
$row['webform_id'] = $log_webform->toLink($log_webform->label(), 'results-log');
}
if (empty($source_entity)) {
$entity = NULL;
if ($log->entity_type && $log->entity_id) {
$entity_type = $log->entity_type;
$entity_id = $log->entity_id;
if ($entity = $this->entityTypeManager()->getStorage($entity_type)->load($entity_id)) {
$row['entity'] = ($entity->hasLinkTemplate('canonical')) ? $entity->toLink() : "$entity_type:$entity_id";
}
else {
$row['entity'] = "$entity_type:$entity_id";
}
}
else {
$row['entity'] = '';
}
}
if (empty($webform_submission)) {
$log_webform_submission = $this->webformSubmissionStorage->load($log->sid);
$route_name = $this->requestHandler->getRouteName($log_webform_submission, $source_entity, 'webform_submission.log');
$route_parameters = $this->requestHandler->getRouteParameters($log_webform_submission, $source_entity);
$row['sid'] = [
'data' => [
'#type' => 'link',
'#title' => $log->sid,
'#url' => Url::fromRoute($route_name, $route_parameters ),
],
];
}
$row['handler_id'] = $log->handler_id;
$row['operation'] = $log->operation;
$row['message'] = $log->message;
$row['uid'] = [
'data' => [
'#theme' => 'username',
'#account' => $this->userStorage->load($log->uid),
],
];
$row['timestamp'] = $this->dateFormatter->format($log->timestamp, 'short');
$rows[] = $row;
}
$build['table'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No log messages available.'),
];
$build['pager'] = ['#type' => 'pager'];
return $build;
}
}
......@@ -64,6 +64,7 @@ use Drupal\webform\WebformSubmissionStorageInterface;
* "results-submissions" = "/admin/structure/webform/manage/{webform}/results/submissions",
* "results-table" = "/admin/structure/webform/manage/{webform}/results/table",
* "results-export" = "/admin/structure/webform/manage/{webform}/results/download",
* "results-log" = "/admin/structure/webform/manage/{webform}/results/log",
* "results-clear" = "/admin/structure/webform/manage/{webform}/results/clear",
* "collection" = "/admin/structure/webform",
* },
......
......@@ -53,6 +53,7 @@ use Drupal\webform\WebformSubmissionInterface;
* "table" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/table",
* "text" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/text",
* "yaml" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/yaml",
* "yaml" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/yaml",
* "edit-form" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/edit",
* "notes-form" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/notes",
* "resend-form" = "/admin/structure/webform/manage/{webform}/submission/{webform_submission}/resend",
......@@ -91,6 +92,13 @@ class WebformSubmission extends ContentEntityBase implements WebformSubmissionIn
*/
protected $originalData = [];
/**
* Flag to indicated is submission is being converted from anonymous to authenticated.
*
* @var bool
*/
protected $converting = FALSE;
/**
* {@inheritdoc}
*/
......@@ -195,7 +203,7 @@ class WebformSubmission extends ContentEntityBase implements WebformSubmissionIn
* {@inheritdoc}
*/
public function serial() {
return $this->serial->value;
return $this->get('serial')->value;
}
/**
......@@ -485,6 +493,13 @@ class WebformSubmission extends ContentEntityBase implements WebformSubmissionIn
return $this->get('in_draft')->value ? TRUE : FALSE;
}
/**
* {@inheritdoc}
*/
public function isConverting() {
return $this->converting;
}
/**
* {@inheritdoc}
*/
......@@ -510,13 +525,16 @@ class WebformSubmission extends ContentEntityBase implements WebformSubmissionIn
* Track the state of a submission.
*
* @return int
* Either STATE_UNSAVED, STATE_DRAFT, STATE_COMPLETED, or STATE_UPDATED,
* Either STATE_UNSAVED, STATE_CONVERTED, STATE_DRAFT, STATE_COMPLETED, or STATE_UPDATED,
* depending on the last save operation performed.
*/
public function getState() {
if (!$this->id()) {
return self::STATE_UNSAVED;
}
elseif ($this->isConverting()) {
return self::STATE_CONVERTED;
}
elseif ($this->isDraft()) {
return self::STATE_DRAFT;
}
......@@ -655,6 +673,16 @@ class WebformSubmission extends ContentEntityBase implements WebformSubmissionIn
return parent::save();
}
/**
* {@inheritdoc}
*/
public function convert(UserInterface $account) {
$this->converting = TRUE;
$this->setOwner($account);
$this->save();
$this->converting = FALSE;
}
/**
* {@inheritdoc}
*/
......
......@@ -11,6 +11,7 @@ use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\file\Entity\File;
use Drupal\webform\Element\WebformMessage;
use Drupal\webform\Element\WebformSelectOther;
......@@ -19,7 +20,6 @@ use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\WebformElementManagerInterface;
use Drupal\webform\WebformHandlerBase;
use Drupal\webform\WebformHandlerMessageInterface;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionInterface;
use Drupal\webform\WebformTokenManagerInterface;
use Psr\Log\LoggerInterface;
......@@ -99,8 +99,8 @@ class EmailWebformHandler extends WebformHandlerBase implements WebformHandlerMe
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger, AccountInterface $current_user, ConfigFactoryInterface $config_factory, MailManagerInterface $mail_manager, WebformTokenManagerInterface $token_manager, WebformElementManagerInterface $element_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $logger);
public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, ConfigFactoryInterface $config_factory, MailManagerInterface $mail_manager, WebformTokenManagerInterface $token_manager, WebformElementManagerInterface $element_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $logger, $entity_type_manager);
$this->currentUser = $current_user;
$this->configFactory = $config_factory;
$this->mailManager = $mail_manager;
......@@ -117,6 +117,7 @@ class EmailWebformHandler extends WebformHandlerBase implements WebformHandlerMe
$plugin_id,
$plugin_definition,
$container->get('logger.factory')->get('webform.email'),
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('config.factory'),
$container->get('plugin.manager.mail'),
......@@ -426,6 +427,7 @@ class EmailWebformHandler extends WebformHandlerBase implements WebformHandlerMe
'#title' => $this->t('Send email'),
'#options' => [
WebformSubmissionInterface::STATE_DRAFT => $this->t('...when <b>draft</b> is saved.'),
WebformSubmissionInterface::STATE_CONVERTED => $this->t('...when anonymous submission is <b>converted</b> to authenticated.'),
WebformSubmissionInterface::STATE_COMPLETED => $this->t('...when submission is <b>completed</b>.'),
WebformSubmissionInterface::STATE_UPDATED => $this->t('...when submission is <b>updated</b>.'),
WebformSubmissionInterface::STATE_DELETED => $this->t('...when submission is <b>deleted</b>.'),
......
<?php
namespace Drupal\webform\Plugin\WebformHandler;
use Drupal\webform\WebformHandlerBase;
use Drupal\webform\WebformSubmissionInterface;
/**
* Webform submission test log handler.
*
* @WebformHandler(
* id = "log",
* label = @Translation("Log"),
* category = @Translation("Logging"),
* description = @Translation("Logs when a draft or submission is created, converted, and updated."),
* cardinality = \Drupal\webform\WebformHandlerInterface::CARDINALITY_SINGLE,
* results = \Drupal\webform\WebformHandlerInterface::RESULTS_IGNORED,
* )
*/
class LogWebformHandler extends WebformHandlerBase {
/**
* {@inheritdoc}
*/
public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) {
$t_args = [
'@title' => $webform_submission->label(),
];
switch ($webform_submission->getState()) {
case WebformSubmissionInterface::STATE_DRAFT:
if ($update) {
$operation = 'draft updated';
$message = $this->t('@title draft updated.', $t_args);
}
else {
$operation = 'draft created';
$message = $this->t('@title draft created.', $t_args);
}
break;
case WebformSubmissionInterface::STATE_COMPLETED:
if ($update) {
$operation = 'submission completed';
$message = $this->t('@title completed using saved draft.', $t_args);
}
else {
$operation = 'submission created';
$message = $this->t('@title created.', $t_args);
}
break;
case WebformSubmissionInterface::STATE_CONVERTED:
$operation = 'submission converted';
$message = $this->t('@title converted from anonymous to @user.', $t_args + ['@user' => $webform_submission->getOwner()->label()]);
break;
case WebformSubmissionInterface::STATE_UPDATED:
$operation = 'submission updated';
$message = $this->t('@title updated.', $t_args);
break;
default:
throw new \Exception('Unexpected webform submission state');
break;
}
$this->log($webform_submission, $operation, $message);
}
/**
* Log a webform handler's submission operation.
*
* @param \Drupal\webform\WebformSubmissionInterface $webform_submission
* A webform submission.
* @param string $operation
* The operation to be logged.
* @param string $message
* The message to be logged.
* @param array $data
* The data to be saved with log record.
*/
protected function log(WebformSubmissionInterface $webform_submission, $o