Skip to content
Snippets Groups Projects
Commit 1953d02d authored by Bryan Sharpe's avatar Bryan Sharpe
Browse files

Issue #3253866 by b_sharpe: Move Data definitions into Plugins

parent a795ec77
Branches
Tags
1 merge request!3Issue #3253866: Move Data definitions into Plugins
Showing
with 1023 additions and 292 deletions
......@@ -15,4 +15,5 @@
data: drupalSettings.usage_data.data
});
});
})(jQuery, Drupal, drupalSettings);
<?php
namespace Drupal\usage_data\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines usage_type annotation object.
*
* @Annotation
*/
class UsageType extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The description of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
}
<?php
namespace Drupal\usage_data;
use Drupal\Core\Database\Connection;
use Drupal\Core\State\StateInterface;
use Drupal\usage_data\Event\RecordingViewEvent;
use Drupal\usage_data\Event\UsageDataEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Entity statistics storage.
*/
class EntityStatisticsStorage implements EntityStatisticsStorageInterface {
const TABLE_NAME = 'usage_data';
/**
* The database connection used.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs the entity statistics storage.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection for the node view storage.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Event dispatcher.
*/
public function __construct(Connection $connection, StateInterface $state, RequestStack $request_stack, EventDispatcherInterface $event_dispatcher) {
$this->connection = $connection;
$this->state = $state;
$this->requestStack = $request_stack;
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public function recordView($data) {
$data['count'] = 1;
$data['timestamp'] = $this->getRequestTime();
// Right before we dispatch our event let decode the json into an array so
// that other module can retrieve their data and clean it up.
if (!empty($data['extra_data'])) {
$data['extra_data'] = json_decode($data['extra_data'], TRUE);
}
// This allows other modules to extract the extra data for example and
// assign it to the proper column.
$event = new RecordingViewEvent($data);
$this->eventDispatcher->dispatch(UsageDataEvents::RECORD_VIEW, $event);
$data = $event->getData();
// Removing extra data.
if (isset($data['extra_data'])) {
unset($data['extra_data']);
}
return (bool) $this->connection
->insert(self::TABLE_NAME)
->fields($data)
->execute();
}
/**
* Get current request time.
*
* @return int
* Unix timestamp for current server request time.
*/
protected function getRequestTime() {
return $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME');
}
}
<?php
namespace Drupal\usage_data;
/**
* Entity statistics storage interface.
*/
interface EntityStatisticsStorageInterface {
/**
* Count a entity view.
*
* @param array $data
* The ID of the entity to count.
*
* @return bool
* TRUE if the entity view has been counted.
*/
public function recordView(array $data);
}
......@@ -18,7 +18,7 @@ final class UsageDataEvents {
* @Event
*
* @see \Drupal\usage_data\Event\RecordingViewEvent
* @see \Drupal\usage_data\EntityStatisticsStorage::recordView()
* @see \Drupal\usage_data\UsageDataStorage::recordView()
*
* @var string
*/
......
<?php
namespace Drupal\usage_data\Plugin\UsageType;
use Drupal\usage_data\Plugin\UsageTypePluginBase;
use Drupal\usage_data\UsageDataInterface;
/**
* Plugin implementation of the Genice UsageType.
*
* @UsageType(
* id = "generic",
* label = @Translation("Generic Usage Data"),
* description = @Translation("Tracks all events of all types with additional
* user and role data.")
* )
*/
class Generic extends UsageTypePluginBase {
/**
* The most common front-end viewable entities.
*/
const ENTITY_TYPES = ['node', 'taxonomy_term', 'media', 'commerce_product'];
/**
* Only track full/default view modes.
*/
const ENTITY_VIEW_MODES = ['default', 'full'];
/**
* {@inheritDoc}
*/
public function parseEvent($eventType, $entityTypeId, $entityId, array &$render, $viewMode = FALSE) {
if ((in_array($entityTypeId, self::ENTITY_TYPES) && in_array($viewMode, self::ENTITY_VIEW_MODES)) || in_array($entityTypeId, UsageDataInterface::OTHER_ENTITY_TYPES)) {
return $this->defaultData($eventType, $entityTypeId, $entityId);
}
return FALSE;
}
/**
* {@inheritDoc}
*/
public static function validateEvent(array &$data) {
$allowed_Types = self::ENTITY_TYPES + UsageDataInterface::OTHER_ENTITY_TYPES;
if (!in_array($data['entity_type_id'], $allowed_Types)) {
$data['skip'] = TRUE;
}
parent::validateEvent($data);
}
}
<?php
namespace Drupal\usage_data\Plugin\UsageType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\usage_data\Plugin\UsageTypePluginBase;
use Drupal\usage_data\UsageDataInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the Legacy UsageType.
*
* @UsageType(
* id = "legacy",
* label = @Translation("Supports Usage Data from V1."),
* description = @Translation("Tracks all events of all types with additional
* user and role data.")
* )
*/
class Legacy extends UsageTypePluginBase {
/**
* Only track full/default view modes.
*/
const ENTITY_VIEW_MODES = ['default', 'full'];
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected AccountProxyInterface $currentUser;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entityTypeManager, CurrentPathStack $currentPath, AccountProxyInterface $currentUser) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entityTypeManager, $currentPath);
$this->currentUser = $currentUser;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('path.current'),
$container->get('current_user')
);
}
/**
* Adds user information to the base tables.
*/
public static function schema() {
return [
'fields' => [
'uid' => [
'description' => 'The user ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'user_name' => [
'description' => 'The username of the user.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
'user_role' => [
'description' => 'The role of the user.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
],
'indexes' => [
'uid' => ['uid'],
],
];
}
/**
* {@inheritDoc}
*
* @todo fix for page manager and variant.
*/
public function parseEvent($eventType, $entityTypeId, $entityId, array &$render, $viewMode = FALSE) {
if (!in_array($entityTypeId, UsageDataInterface::OTHER_ENTITY_TYPES) && !in_array($viewMode, self::ENTITY_VIEW_MODES)) {
return FALSE;
}
$data = $this->defaultData($eventType, $entityTypeId, $entityId);
// V1 didn't use file entity here.
if ($eventType == 'download') {
$data['entity_type_id'] = 'file_download';
}
// Add user data.
$roles = $this->currentUser->getRoles(TRUE);
$data['uid'] = $this->currentUser->id();
$data['user_name'] = $this->currentUser->getAccountName();
$data['user_role'] = $roles ? implode(',', $roles) : '';
// Because we're tracking users, we need to alter the render array to cache
// per user context.
$render['#cache']['context'][] = 'user';
// V1 considered media as a download.
if ($entityTypeId === 'media') {
$data['event_type'] = UsageDataInterface::EVENT_TYPE_DOWNLOAD;
}
return $data;
}
/**
* {@inheritDoc}
*/
public static function validateEvent(&$data) {
parent::validateEvent($data);
// If empty then every role can be tracked for view.
$data['uid'] = (int) $data['uid'];
// Regex validating alphanumeric,
// underscore and commas which can be present if separating multiple roles.
$current_user_roles = filter_var(
$data['user_role'],
FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[A-Za-z0-9_,]+$/']]
);
$data['user_role'] = $current_user_roles;
$data['user_name'] = filter_var($data['user_name'], FILTER_SANITIZE_STRING);
}
}
<?php
namespace Drupal\usage_data\Plugin;
/**
* Interface for usage_type plugins.
*/
interface UsageTypeInterface {
/**
* Returns the translated plugin label.
*
* @return string
* The translated title.
*/
public function label();
/**
* Get the id of the usage type.
*
* @return string
* The plugin id.
*/
public function id();
/**
* Get a list of evebt types that this provider creates.
*
* @return array
* The event types.
*/
public function eventTypes();
/**
* Provides additional schema requirements.
*
* @return array
* Additions to the default schema.
*/
public static function schema();
/**
* Validate and optionally skip data usage.
*
* @param array $data
* The data to parse.
*/
public static function validateEvent(array &$data);
/**
* Forumlate data to be inserted.
*
* @param string $eventType
* The event type.
* @param string $entityTypeId
* The entity type id.
* @param mixed $entityId
* The entity id.
* @param array $render
* The render array of the element.
* @param mixed $viewMode
* The view mode if an entity is being passed.
*
* @return array
* The populated usage data.
*/
public function parseEvent($eventType, $entityTypeId, $entityId, array &$render, $viewMode = FALSE);
/**
* Provides the string to append to the table name.
*
* @return string
* The suffix of the table to store usage.
*/
public function tableSuffix();
}
<?php
namespace Drupal\usage_data\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Provides an Archiver plugin manager.
*/
class UsageTypeManager extends DefaultPluginManager implements UsageTypeManagerInterface {
/**
* Constructs a UsageTypeManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct(
'Plugin/UsageType',
$namespaces,
$module_handler,
'Drupal\usage_data\Plugin\UsageTypeInterface',
'Drupal\usage_data\Annotation\UsageType'
);
$this->alterInfo('usage_type_info');
$this->setCacheBackend($cache_backend, 'usage_type_plugins');
}
}
<?php
namespace Drupal\usage_data\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Provides an interface for the UsageTypeManager.
*/
interface UsageTypeManagerInterface extends PluginManagerInterface {
}
<?php
namespace Drupal\usage_data\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base class for notification_provider plugins.
*/
abstract class UsageTypePluginBase extends PluginBase implements UsageTypeInterface, ContainerFactoryPluginInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected CurrentPathStack $currentPath;
/**
* Constructs a new plugin base.
*
* @param array $configuration
* The configuration.
* @param string $plugin_id
* The plugin id.
* @param mixed $plugin_definition
* The plugin devinition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Path\CurrentPathStack $currentPath
* The current path service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entityTypeManager, CurrentPathStack $currentPath) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entityTypeManager;
$this->currentPath = $currentPath;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('path.current')
);
}
/**
* {@inheritdoc}
*/
public function label() {
// Cast the label to a string since it is a TranslatableMarkup object.
return (string) $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function id() {
return (string) $this->pluginDefinition['id'];
}
/**
* {@inheritdoc}
*/
public function eventTypes() {
return [];
}
/**
* {@inheritdoc}
*/
public static function schema() {
return [];
}
/**
* {@inheritdoc}
*
* @todo does this possibly need more to prevent corrupt posts?
*/
public static function validateEvent(array &$data) {
// @todo utilize hook_usage_data_event_types() once built.
if (!in_array($data['event_type'], ['view', 'download', 'click'])) {
$data['skip'] = TRUE;
}
$data['entity_type_id'] = filter_var($data['entity_type_id'], FILTER_SANITIZE_STRING);
$data['entity_id'] = filter_var($data['entity_id'], FILTER_SANITIZE_STRING);
$data['path'] = filter_var($data['path'], FILTER_SANITIZE_URL);
}
/**
* {@inheritdoc}
*
* @todo this would need to be static if used in storage, maybe make annotated?
*/
public function tableSuffix() {
return $this->id();
}
/**
* Constructs the usual defaults for data usage.
*
* @param string $eventType
* The event type.
* @param string $entityTypeId
* The entity type id.
* @param string $entityId
* The entity id.
*
* @return array
* The default data.
*/
protected function defaultData($eventType, $entityTypeId, $entityId) {
return [
'event_type' => $eventType,
'entity_type_id' => $entityTypeId,
'entity_id' => $entityId,
'path' => $this->currentPath->getPath(),
];
}
}
<?php
namespace Drupal\usage_data;
use Drupal\Core\Url;
use Drupal\usage_data\Plugin\UsageTypeManagerInterface;
/**
* The usage data service.
*
* A utility service to assist with tracking usage data.
*/
class UsageData implements UsageDataInterface {
/**
* Usage type manager.
*
* @var \Drupal\usage_data\Plugin\UsageTypeManagerInterface
*/
protected UsageTypeManagerInterface $usageTypeManager;
/**
* Constructs a new usage data object.
*
* @param \Drupal\usage_data\Plugin\UsageTypeManagerInterface $usageTypeManager
* The usage type manager.
*/
public function __construct(UsageTypeManagerInterface $usageTypeManager) {
$this->usageTypeManager = $usageTypeManager;
}
/**
* {@inheritDoc}
*/
public function getUsageData($eventType, $entityTypeId, $entityId, &$render, $viewMode = FALSE) {
$data = [];
$plugin_definitions = $this->usageTypeManager->getDefinitions();
// @todo need to check status,
// see https://www.drupal.org/project/usage_data/issues/3253891.
foreach ($plugin_definitions as $definition) {
/** @var \Drupal\usage_data\Plugin\UsageTypeInterface $plugin */
$plugin = $this->usageTypeManager->createInstance($definition['id']);
if ($parsed = $plugin->parseEvent($eventType, $entityTypeId, $entityId, $render, $viewMode)) {
// @todo add back the dispatched event for extra data.
$data[$plugin->id()] = $parsed;
}
}
return $data;
}
/**
* {@inheritDoc}
*/
public function getPostUrl() {
$path = drupal_get_path('module', 'usage_data');
return Url::fromUri('base:' . $path . '/usage_data.php')->toString();
}
}
<?php
namespace Drupal\usage_data;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Database\Connection;
use Drupal\usage_data\Plugin\UsageTypeManagerInterface;
/**
* Usage data database storage.
*
* For performance, it has been attempted to not instantiate classes where
* possible and instead use static methods.
*/
class UsageDataDatabaseStorage implements UsageDataStorageInterface {
/**
* Sets the base table name.
*/
const TABLE_BASE = 'usage_data';
/**
* Support for v1.
*/
const LEGACY_TYPE = 'legacy';
/**
* The database connection used.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The usage type manager.
*
* @var \Drupal\usage_data\Plugin\UsageTypeManagerInterface
*/
protected UsageTypeManagerInterface $usageTypeManager;
/**
* Constructs the database storage.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection for the node view storage.
* @param \Drupal\usage_data\Plugin\UsageTypeManagerInterface $usageTypeManager
* The usage type manager.
*/
public function __construct(Connection $connection, UsageTypeManagerInterface $usageTypeManager) {
$this->connection = $connection;
$this->usageTypeManager = $usageTypeManager;
}
/**
* {@inheritdoc}
*/
public function recordUsage(array $data) {
foreach ($data as $type => $values) {
// @todo log on failure?
$this->recordUsageByType($type, $values);
}
}
/**
* {@inheritdoc}
*/
public function recordUsageByType($type, array $data) {
try {
$query = $this->connection
->insert($this->tableName($type))
->fields(array_keys($data[0]));
foreach ($data as $fields) {
// Only pass the values since the order of $fields matches the order of
// the insert fields. This is a performance optimization to avoid
// unnecessary loops within the method.
$query->values(array_values($fields));
}
return (bool) $query->execute();
}
catch (\Exception $e) {
$database_schema = $this->connection->schema();
if ($database_schema->tableExists($this->tableName($type))) {
throw $e;
}
else {
$this->createTable($type);
$this->recordUsageByType($type, $data);
}
}
return FALSE;
}
/**
* {@inheritDoc}
*/
public function createTable($type) {
$table = $this->tableName($type);
$schema = NestedArray::mergeDeep($this->defaultSchema(), $this->usageTypeSchema($type));
if (!$this->connection->schema()->tableExists($this->tableName($type))) {
$this->connection->schema()->createTable($table, $schema);
}
}
/**
* {@inheritdoc}
*/
public function dropTable($type) {
if ($this->connection->schema()->tableExists($this->tableName($type))) {
$this->connection->schema()->dropTable($this->tableName($type));
}
}
/**
* {@inheritDoc}
*/
public function defaultSchema() {
return [
'description' => 'Access usage data.',
'fields' => [
'id' => [
'description' => 'The identifier for the schema.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'event_type' => [
'description' => 'The event type.',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
],
'entity_type_id' => [
'description' => 'The entity type id.',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
],
'entity_id' => [
'description' => 'The entity id for these statistics.',
'type' => 'varchar_ascii',
'not null' => TRUE,
'default' => '',
'length' => '255',
],
'path' => [
'type' => 'text',
'not null' => FALSE,
'description' => 'Path of the event.',
],
'timestamp' => [
'description' => 'The most recent time the event took place.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
],
'primary key' => ['id'],
'indexes' => [
'entity_type_id' => ['entity_type_id'],
'entity_id' => ['entity_id'],
],
];
}
/**
* Loads the schema specific to the usage plugin.
*
* @param string $type
* The usage type.
*
* @return array
* The schema provided by the usage type.
*/
protected function usageTypeSchema($type) {
$definitions = $this->usageTypeManager->getDefinitions();
if (!empty($definitions[$type])) {
$plugin_class = $definitions[$type]['class'];
return $plugin_class::schema();
}
return [];
}
/**
* Act on an exception when the table might not have been created.
*
* If the table does not yet exist, that's fine, but if the table exists and
* something else caused the exception, then propagate it.
*
* @param string $type
* The usage plugin type.
* @param \Exception $e
* The exception.
*
* @throws \Exception
*/
protected function catchException($type, \Exception $e) {
if ($this->connection->schema()->tableExists($this->tableName($type))) {
throw $e;
}
}
/**
* Generates the table name from the entity type.
*
* @param string $type
* The usage plugin type.
*
* @return string
* The table name.
*/
protected function tableName($type) {
// @todo use plugin table suffix here.
return $type === self::LEGACY_TYPE ? self::TABLE_BASE : self::TABLE_BASE . '_' . trim(strtolower($type));
}
}
<?php
namespace Drupal\usage_data;
/**
* Interface definition for the main service.
*/
interface UsageDataInterface {
/**
* Special cases to track page views.
*/
const OTHER_ENTITY_TYPES = ['views', 'route'];
/**
* Default event type "view".
*/
const EVENT_TYPE_VIEW = 'view';
/**
* Default event type "click".
*/
const EVENT_TYPE_CLICK = 'click';
/**
* Default event type "download".
*/
const EVENT_TYPE_DOWNLOAD = 'download';
/**
* Retrieve the fully populated event data.
*
* @param string $eventType
* The event type.
* @param string $entityTypeId
* The entity type id.
* @param mixed $entityId
* The entity id.
* @param array $render
* The render array of the element.
* @param mixed $viewMode
* The view mode if an entity is being passed.
*
* @return array
* The populated usage data.
*/
public function getUsageData($eventType, $entityTypeId, $entityId, array &$render, $viewMode = FALSE);
/**
* Helper to retrieve post path.
*
* @return \Drupal\Core\GeneratedUrl|string
* The path to the tracking script.
*/
public function getPostUrl();
}
<?php
namespace Drupal\usage_data;
/**
* Usage Data storage interface.
*/
interface UsageDataStorageInterface {
/**
* Records Usage keyed by type.
*
* @param array $data
* The data to insert.
*/
public function recordUsage(array $data);
/**
* Record typed usage.
*
* @param string $type
* The type of usage to store.
* @param array $data
* The data to insert.
*
* @return bool
* TRUE if the usage has been stored.
*/
public function recordUsageByType($type, array $data);
/**
* Defines the default schema for usage plugins.
*
* @return array
* The default schema.
*/
public function defaultSchema();
/**
* Creates a table by type.
*
* @param string $type
* The type of usage.
*/
public function createTable($type);
/**
* Drops a table by type.
*
* @param string $type
* The type of usage.
*/
public function dropTable($type);
}
......@@ -5,89 +5,12 @@
* Install and update functions for the Work Horse Statistics module.
*/
// phpcs:disable
/**
* Implements hook_schema().
* Implements hook_uninstall().
*/
function usage_data_schema() {
$schema['usage_data'] = [
'description' => 'Access usage data.',
'fields' => [
'id' => [
'description' => 'The identifier for the schema.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'event_type' => [
'description' => 'The event type, either view or download.',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
],
'entity_id' => [
'description' => 'The entity id for these statistics.',
'type' => 'varchar_ascii',
'not null' => TRUE,
'default' => '',
'length' => '255',
],
'entity_type_id' => [
'description' => 'The entity type id.',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
],
'path' => [
'type' => 'text',
'not null' => FALSE,
'description' => 'Path of the event.',
],
'count' => [
'description' => 'Simple count for each event.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
'size' => 'small',
],
'timestamp' => [
'description' => 'The most recent time the {entity} has been viewed.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'uid' => [
'description' => 'The user ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'user_name' => [
'description' => 'The username of the user.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
'user_role' => [
'description' => 'The role of the user.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
],
'primary key' => ['id'],
'indexes' => [
'entity_id' => ['entity_id'],
'entity_type_id' => ['entity_type_id'],
'uid' => ['uid'],
],
];
return $schema;
function usage_data_uninstall($is_syncing) {
// @todo delete tables. see UsageDataStorageInterface::dropTable().
}
// phpcs:enable
......@@ -5,13 +5,12 @@
* Contains usage_data.module.
*/
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\Core\Url;
use Drupal\usage_data\Event\CollectExtraDataEvent;
use Drupal\usage_data\Event\UsageDataEvents;
use Drupal\usage_data\UsageDataInterface;
/**
* Implements hook_help().
......@@ -29,18 +28,27 @@ function usage_data_help($route_name, RouteMatchInterface $route_match) {
}
}
/**
* Implements hook_page_attachments().
*/
function usage_data_page_attachments(array &$attachments) {
/** @var \Drupal\usage_data\UsageDataInterface $usage */
$usage = \Drupal::service('usage_data.usage');
$attachments['#attached']['drupalSettings']['usage_data']['url'] = $usage->getPostUrl();
}
/**
* Implements hook_entity_view() for usage_event.
*/
function usage_data_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if (($view_mode === 'full' || $view_mode === 'default') && !$entity->isNew()) {
$build['#attached']['library'][] = 'usage_data/entity.statistics';
$entity_type_id = $entity->getEntityTypeId();
$settings = _usage_data_default_settings($entity->id(), $entity_type_id);
if ($entity_type_id === 'media') {
$settings['data']['event_type'] = 'download';
if (!$entity->isNew()) {
/** @var \Drupal\usage_data\UsageDataInterface $usage */
$usage = \Drupal::service('usage_data.usage');
if ($data = $usage->getUsageData(UsageDataInterface::EVENT_TYPE_VIEW, $entity->getEntityTypeId(), $entity->id(), $build, $view_mode)) {
$build['#attached']['library'][] = 'usage_data/entity.statistics';
// Has to be unique here.
$build['#attached']['drupalSettings']['usage_data']['data'][$entity->uuid() . ':' . $view_mode] = $data;
}
$build['#attached']['drupalSettings']['usage_data'] = $settings;
}
}
......@@ -50,13 +58,19 @@ function usage_data_entity_view(array &$build, EntityInterface $entity, EntityVi
function usage_data_views_pre_render($view) {
/** @var \Drupal\views\ViewExecutable $view */
if ($view->getDisplay()->hasPath()) {
/** @var \Drupal\usage_data\UsageDataInterface $usage */
$usage = \Drupal::service('usage_data.usage');
// For views entity id we are going to concatenate the view id and
// the display id.
// @todo remember to parse the id in order to retrieve the view and the
// display.
$entity_id = $view->storage->id() . ':' . $view->current_display;
$view->element['#attached']['drupalSettings']['usage_data'] = _usage_data_default_settings($entity_id, 'view');
$view->element['#attached']['library'][] = 'usage_data/entity.statistics';
if ($data = $usage->getUsageData(UsageDataInterface::EVENT_TYPE_VIEW, 'views', $entity_id, $view->element)) {
$view->element['#attached']['library'][] = 'usage_data/entity.statistics';
$view->element['#attached']['drupalSettings']['usage_data']['data'][$entity_id] = $data;
}
}
}
......@@ -64,14 +78,16 @@ function usage_data_views_pre_render($view) {
* Implements hook_preprocess_HOOK() for page.
*/
function usage_data_preprocess_page(&$variables) {
// Implement support for page manager.
$current_route = \Drupal::routeMatch()->getRouteObject();
if ($current_route && ($variant_id = $current_route->getDefault('page_manager_page_variant'))) {
// Same approach as for view where we use ':' to concatenate the page
// manager id and the variant id.
$entity_id = $current_route->getDefault('page_manager_page') . ':' . $variant_id;
$variables['#attached']['drupalSettings']['usage_data'] = _usage_data_default_settings($entity_id, 'page');
$variables['#attached']['library'][] = 'usage_data/entity.statistics';
$route_name = \Drupal::routeMatch()->getRouteName();
// Omit views/entity routes.
if (!usage_data_is_entity_route() && strpos($route_name, 'views.') !== 0) {
/** @var \Drupal\usage_data\UsageDataInterface $usage */
$usage = \Drupal::service('usage_data.usage');
if ($data = $usage->getUsageData(UsageDataInterface::EVENT_TYPE_VIEW, 'route', $route_name, $variables)) {
$variables['#attached']['drupalSettings']['usage_data']['data'][$route_name] = $data;
$variables['#attached']['library'][] = 'usage_data/entity.statistics';
}
}
}
......@@ -91,50 +107,38 @@ function usage_data_file_download($uri) {
$roles = $current_user->getRoles(TRUE);
$data['event_type'] = 'download';
$data['entity_id'] = $fid;
$data['entity_type_id'] = 'file_download';
$data['entity_type_id'] = 'file';
$data['path'] = $target;
$data['uid'] = $current_user->id();
$data['user_name'] = $current_user->getAccountName();
$data['user_role'] = $roles ? implode(',', $roles) : '';
\Drupal::service('usage_data.storage.entity')->recordView($data);
// @todo fix for direct input.
// \Drupal::service('usage_data.storage.entity')->recordViews($data);
}
/**
* Default settings for usage data array.
*
* @param mixed $entity_id
* The entity id.
* @param string $entity_type_id
* The entity type id.
*
* @return array
* The drupal settings data.
* Helper function to extract the entity for the supplied route.
*/
function _usage_data_default_settings($entity_id, $entity_type_id) : array {
$path = drupal_get_path('module', 'usage_data');
$current_user = \Drupal::currentUser();
$roles = $current_user->getRoles(TRUE);
$data = [
'event_type' => 'view',
'entity_id' => $entity_id,
'entity_type_id' => $entity_type_id,
'path' => \Drupal::service('path.current')->getPath(),
'uid' => $current_user->id(),
'user_name' => $current_user->getAccountName(),
'user_role' => $roles ? implode(',', $roles) : '',
];
function usage_data_is_entity_route() {
$route_match = \Drupal::routeMatch();
// Dispatching this event here so that other module can populate the other
// data field. These other module will also be responsible to clean it up
// prior to record being inserted.
$event_dispatcher = \Drupal::service('event_dispatcher');
$extra_data = [];
$event = new CollectExtraDataEvent($extra_data);
$event_dispatcher->dispatch(UsageDataEvents::COLLECT_EXTRA_DATA, $event);
$data['extra_data'] = json_encode($event->getExtraData());
// @todo this likely doesn't apply to all entities.
if (strpos($route_match->getRouteName(), 'canonical') === FALSE) {
return FALSE;
}
return [
'data' => $data,
'url' => Url::fromUri('base:' . $path . '/usage_data.php')->toString(),
];
// Entity will be found in the route parameters.
if (($route = $route_match->getRouteObject()) && ($parameters = $route->getOption('parameters'))) {
// Determine if the current route represents an entity.
foreach ($parameters as $name => $options) {
if (isset($options['type']) && strpos($options['type'], 'entity:') === 0) {
$entity = $route_match->getParameter($name);
if ($entity instanceof ContentEntityInterface) {
return TRUE;
}
return FALSE;
}
}
}
return FALSE;
}
......@@ -6,6 +6,8 @@
*/
use Drupal\Core\DrupalKernel;
use Drupal\usage_data\Event\RecordingViewEvent;
use Drupal\usage_data\Event\UsageDataEvents;
use Symfony\Component\HttpFoundation\Request;
// Work when this module is at web/modules/contrib/<module_name>.
......@@ -15,29 +17,60 @@ $kernel = DrupalKernel::createFromRequest(Request::createFromGlobals(), $autoloa
$kernel->boot();
$container = $kernel->getContainer();
// If empty then every role can be tracked for view.
$data['uid'] = filter_input(INPUT_POST, 'uid', FILTER_VALIDATE_INT);
$request = $container->get('request_stack');
$request->push(Request::createFromGlobals());
$event_dispatcher = $container->get('event_dispatcher');
$usage_plugin_manager = $container->get('plugin.manager.usage_type');
$database_storage = $container->get('usage_data.storage.database');
$post = $request->getCurrentRequest()->request->all();
// Regex validating alphanumeric,
// underscore and commas which can be present when separating multiple roles.
$current_user_roles = filter_input(
INPUT_POST,
'user_role',
FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[A-Za-z0-9_,]+$/']]
);
// Parse post data.
if ($request && $event_dispatcher && $usage_plugin_manager && is_array($post)) {
$request_time = $request->getCurrentRequest()->server->get('REQUEST_TIME');
$definitions = $usage_plugin_manager->getDefinitions();
$data['user_role'] = $current_user_roles;
// For performance, we'll be doing multi-row inserts here.
$insert = [];
foreach ($post as $data) {
foreach ($definitions as $id => $definition) {
if (isset($data[$id])) {
$usage = $data[$id];
$class = $definition['class'];
$class::validateEvent($usage);
if (!empty($usage['skip'])) {
// @todo maybe log this?
continue;
}
$data['event_type'] = filter_input(INPUT_POST, 'event_type');
if (!in_array($data['event_type'], ['view', 'download'])) {
$data['event_type'] = 'view';
// Timestamp is same for all.
$usage['timestamp'] = $request_time;
// Right before we dispatch our event let decode the json into an array
// so that other module can retrieve their data and clean it up.
// @todo not sure this is really needed now that plugins control data.
if (!empty($usage['extra_data'])) {
$usage['extra_data'] = json_decode($usage['extra_data'], TRUE);
}
// This allows other modules to extract the extra data for example and
// assign it to the proper column.
$event = new RecordingViewEvent($usage);
$event_dispatcher->dispatch($event, UsageDataEvents::RECORD_VIEW);
$usage = $event->getData();
// Removing extra data.
if (isset($usage['extra_data'])) {
unset($usage['extra_data']);
}
// Prepare for DB.
$insert[$id][] = $usage;
}
}
}
// Record all applicable data.
if (!empty($insert)) {
$database_storage->recordUsage($insert);
}
}
$data['entity_id'] = filter_input(INPUT_POST, 'entity_id');
$data['entity_type_id'] = filter_input(INPUT_POST, 'entity_type_id');
$data['path'] = filter_input(INPUT_POST, 'path');
$data['user_name'] = filter_input(INPUT_POST, 'user_name');
$data['extra_data'] = filter_input(INPUT_POST, 'extra_data');
$container->get('request_stack')->push(Request::createFromGlobals());
$container->get('usage_data.storage.entity')->recordView($data);
services:
usage_data.storage.entity:
class: Drupal\usage_data\EntityStatisticsStorage
arguments: ['@database', '@state', '@request_stack', '@event_dispatcher']
usage_data.storage.database:
class: Drupal\usage_data\UsageDataDatabaseStorage
arguments: ['@database', '@plugin.manager.usage_type']
tags:
- { name: backend_overridable }
plugin.manager.usage_type:
class: Drupal\usage_data\Plugin\UsageTypeManager
parent: default_plugin_manager
usage_data.usage:
class: Drupal\usage_data\UsageData
arguments: ['@plugin.manager.usage_type']
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment