bat-8.x-1.x-dev/bat.module
bat.module
<?php
/**
* @file
* Contains bat.module..
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\Render\Element;
use Drupal\bat\Entity\TypeGroup;
use Drupal\bat\Entity\TypeGroupBundle;
/**
* Implements hook_help().
*/
function bat_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the bat module.
case 'help.page.bat':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('A generalized Booking and Availability Management Framework') . '</p>';
return $output;
default:
}
}
/**
* Implements hook_theme().
*/
function bat_theme() {
return [
'bat_type_group_add_list' => [
'variables' => ['content' => NULL],
],
'bat_entity_edit_form' => [
'render element' => 'form',
],
];
}
/**
* Implements hook_toolbar().
*/
function bat_toolbar() {
$items = [];
$items['bat'] = [
'#type' => 'toolbar_item',
'#attached' => [
'library' => [
'bat/drupal.bat.toolbar',
],
],
];
return $items;
}
/**
* Prepares variables for list of available type group bundles templates.
*
* Default template: bat-type-group-add-list.html.twig.
*
* @param array $variables
* An associative array containing:
* - content: An array of type group bundles.
*/
function template_preprocess_bat_type_group_add_list(array &$variables) {
$variables['types'] = [];
if (!empty($variables['content'])) {
foreach ($variables['content'] as $type) {
$variables['types'][$type->id()] = [
'type' => $type->id(),
'add_link' => Link::fromTextAndUrl($type->label(), new Url('entity.bat_type_group.add_form', ['type_group_bundle' => $type->id()])),
];
}
}
}
/**
* Implements hook_entity_access().
*/
function bat_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
$rights = &drupal_static(__FUNCTION__, []);
$entity_type = $entity->getEntityType()->id();
if (in_array($entity_type, ['bat_type_group', 'bat_unit', 'bat_unit_type', 'bat_event', 'bat_event_series', 'bat_booking'])) {
$entity_info = \Drupal::entityTypeManager()->getDefinition($entity_type);
$cid = $entity->id();
// If we are creating a new entity make sure we set the type
// so permissions get applied.
if ($operation == 'create' && $cid == '') {
$cid = $entity->getEntityType()->id();
}
// If we've already checked access for this entity, user and op, return the
// cached result.
if (isset($rights[$account->id()][$cid][$operation])) {
if ($rights[$account->id()][$cid][$operation]) {
return AccessResult::allowed();
}
else {
return AccessResult::forbidden();
}
}
// Grant generic administrator level access.
if ($account->hasPermission('bypass ' . $entity_type . ' entities access')) {
$rights[$account->id()][$cid][$operation] = TRUE;
return AccessResult::allowed();
}
if ($operation == 'view') {
if (Drupal::moduleHandler()->hasImplementations('query_' . $entity_type . '_alter') ||
Drupal::moduleHandler()->hasImplementations('query_' . $entity_type . '_access_alter')) {
$query = \Drupal::database()->select($entity_info->getBaseTable());
$query->addExpression('1');
$result = (bool) $query
->addTag($entity_type)
->addTag($entity_type . '_access')
->addMetaData('account', $account)
->condition($entity_info->getKey('id'), $entity->id())
->range(0, 1)
->execute()
->fetchField();
$rights[$account->id()][$cid][$operation] = $result;
if ($result) {
return AccessResult::allowed();
}
else {
return AccessResult::forbidden();
}
}
else {
$rights[$account->id()][$cid][$operation] = TRUE;
return AccessResult::allowed();
}
}
else {
// Non-view operations.
// First grant access to the entity for the specified operation if no other
// module denies it and at least one other module says to grant access.
$access_results = \Drupal::moduleHandler()->invokeAll('bat_entity_access', [$entity, $operation, $account]);
if (in_array(FALSE, $access_results, TRUE)) {
$rights[$account->id()][$cid][$operation] = FALSE;
return AccessResult::forbidden();
}
elseif (in_array(TRUE, $access_results, TRUE)) {
$rights[$account->id()][$cid][$operation] = TRUE;
return AccessResult::allowed();
}
// Grant access based on entity type and bundle specific permissions with
// special handling for the create operation since the entity passed in will
// be initialized without ownership.
if ($operation == 'create') {
$access = $account->hasPermission('create ' . $entity_type . ' entities') || $account->hasPermission('create ' . $entity_type . ' entities of bundle ' . $entity->bundle());
$rights[$account->id()][$cid][$operation] = $access;
if ($access) {
return AccessResult::allowed();
}
else {
return AccessResult::forbidden();
}
}
else {
// Finally perform checks for the rest of operations. Begin by
// extracting the bundle name from the entity if available.
$bundle_name = $entity->bundle();
// For the update and delete operations, first perform the entity type and
// bundle-level access check for any entity.
if ($account->hasPermission($operation . ' any ' . $entity_type . ' entity') ||
$account->hasPermission($operation . ' any ' . $entity_type . ' entity of bundle ' . $bundle_name)) {
$rights[$account->id()][$cid][$operation] = TRUE;
return AccessResult::allowed();
}
// Then check an authenticated user's access to delete own entities.
if (method_exists($entity, 'getOwnerId')) {
if ($account->id() && $entity->getOwnerId() && $entity->getOwnerId() === $account->id()) {
if ($account->hasPermission($operation . ' own ' . $entity_type . ' entities') ||
$account->hasPermission($operation . ' own ' . $entity_type . ' entities of bundle ' . $bundle_name)) {
$rights[$account->id()][$cid][$operation] = TRUE;
return AccessResult::allowed();
}
}
}
}
}
return AccessResult::forbidden();
}
}
/**
* Return permission names for a given entity type.
*/
function bat_entity_access_permissions($entity_type) {
$entity_info = \Drupal::entityTypeManager()->getDefinition($entity_type);
$label = $entity_info->getLabel()->__toString();
$permissions = [];
// General 'bypass' permission.
$permissions['bypass ' . $entity_type . ' entities access'] = [
'title' => t('Bypass access to @entity_type', ['@entity_type' => $label]),
'description' => t('Allows users to perform any action on @entity_type.', ['@entity_type' => $label]),
'restrict access' => TRUE,
];
// Generic create and edit permissions.
$permissions['create ' . $entity_type . ' entities'] = [
'title' => t('Create @entity_type of any type', ['@entity_type' => $label]),
];
if ($entity_info->getKey('uid') !== FALSE) {
$permissions['view own ' . $entity_type . ' entities'] = [
'title' => t('View own @entity_type of any type', ['@entity_type' => $label]),
];
}
$permissions['view any ' . $entity_type . ' entity'] = [
'title' => t('View any @entity_type of any type', ['@entity_type' => $label]),
'restrict access' => TRUE,
];
if ($entity_info->getKey('uid') !== FALSE) {
$permissions['update own ' . $entity_type . ' entities'] = [
'title' => t('Edit own @entity_type of any type', ['@entity_type' => $label]),
];
}
$permissions['update any ' . $entity_type . ' entity'] = [
'title' => t('Edit any @entity_type of any type', ['@entity_type' => $label]),
'restrict access' => TRUE,
];
if ($entity_info->getKey('uid') !== FALSE) {
$permissions['delete own ' . $entity_type . ' entities'] = [
'title' => t('Delete own @entity_type of any type', ['@entity_type' => $label]),
];
}
$permissions['delete any ' . $entity_type . ' entity'] = [
'title' => t('Delete any @entity_type of any type', [
'@entity_type' => $label,
]),
'restrict access' => TRUE,
];
// Per-bundle create and edit permissions.
foreach (\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type) as $bundle_name => $bundle_info) {
$permissions['create ' . $entity_type . ' entities of bundle ' . $bundle_name] = [
'title' => t('Create %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
];
if ($entity_info->getKey('uid') !== FALSE) {
$permissions['view own ' . $entity_type . ' entities of bundle ' . $bundle_name] = [
'title' => t('View own %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
];
}
$permissions['view any ' . $entity_type . ' entity of bundle ' . $bundle_name] = [
'title' => t('View any %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
'restrict access' => TRUE,
];
if ($entity_info->getKey('uid') !== FALSE) {
$permissions['update own ' . $entity_type . ' entities of bundle ' . $bundle_name] = [
'title' => t('Edit own %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
];
}
$permissions['update any ' . $entity_type . ' entity of bundle ' . $bundle_name] = [
'title' => t('Edit any %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
'restrict access' => TRUE,
];
if ($entity_info->getKey('uid') !== FALSE) {
$permissions['delete own ' . $entity_type . ' entities of bundle ' . $bundle_name] = [
'title' => t('Delete own %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
];
}
$permissions['delete any ' . $entity_type . ' entity of bundle ' . $bundle_name] = [
'title' => t('Delete any %bundle @entity_type', [
'@entity_type' => $label,
'%bundle' => $bundle_info['label'],
]),
'restrict access' => TRUE,
];
}
return $permissions;
}
/**
* Implements hook_query_alter().
*
* Enforces access control for bat units during database queries.
*/
function bat_entity_access_query_alter($query, $entity_type, $base_table = NULL, $account = NULL, $op = 'view') {
// Get the Drupal user account from the query if available, or
// default to the logged in user if not.
if (!isset($account) && !$account = $query->getMetaData('account')) {
$account = \Drupal::currentUser();
}
// Do not apply any conditions for users with administrative view permissions.
if ($account->hasPermission('bypass ' . $entity_type . ' entities access') ||
$account->hasPermission($op . ' any ' . $entity_type . ' entity')) {
return;
}
// Get the entity type info array for the current access check and prepare a
// conditions object.
$entity_info = \Drupal::entityTypeManager()->getDefinition($entity_type);
// Prepare an OR container for conditions. Conditions will be added that seek
// to grant access, meaning any particular type of permission check may grant
// access even if none of the others apply. At the end of this function, if no
// conditions have been added to the array, a condition will be added that
// always returns FALSE (1 = 0).
$conditions = new Condition('OR');
// Loop over every possible bundle for the given entity type.
foreach (\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type) as $bundle_name => $bundle_info) {
// If the user has access to operation entities of the current bundle...
if ($account->hasPermission($op . ' any ' . $entity_type . ' entity of bundle ' . $bundle_name)) {
// Add a condition granting access if the entity specified by the view
// query is of the same bundle.
$conditions->condition($base_table . '.' . $entity_info->getKey('bundle'), $bundle_name);
}
elseif ($account->id() && $entity_info->getKey('uid') !== FALSE &&
$account->hasPermission($op . ' own ' . $entity_type . ' entities of bundle ' . $bundle_name)) {
// Add an AND condition group that grants access if the entity specified
// by the view query matches the same bundle and belongs to the user.
$c_and = new Condition('AND');
$conditions->condition($c_and
->condition($base_table . '.' . $entity_info->getKey('bundle'), $bundle_name)
->condition($base_table . '.' . $entity_info->getKey('uid'), $account->id())
);
}
}
// Perform 'operation own' access control for the entity in the query if the
// user is authenticated.
if ($account->id() && $account->hasPermission($op . ' own ' . $entity_type . ' entities')) {
$conditions->condition($base_table . '.' . $entity_info->getKey('uid'), $account->id());
}
// Prepare an array of condition alter hooks to invoke and an array of context
// data for the current query.
$hooks = [
'bat_entity_access_' . $op . '_condition_' . $entity_type,
'bat_entity_access_' . $op . '_condition',
];
$context = [
'account' => $account,
'entity_type' => $entity_type,
'base_table' => $base_table,
];
// Allow other modules to add conditions to the array as necessary.
\Drupal::moduleHandler()->alter($hooks, $conditions, $context);
// If we have more than one condition based on the entity access permissions
// and any hook implementations...
if (count($conditions)) {
// Add the conditions to the query.
$query->condition($conditions);
}
else {
// Otherwise, since we don't have any possible conditions to match against,
// we falsify this query. View checks are access grants, not access denials.
$query->where('1 = 0');
}
}
/**
* Utility function to create two related datepickers.
*
* We have a few forms that need a start and end date field
* and we need to apply the same javascript to these forms in order to enforce
* specific consistent behaviours and group the form elements and javascript
* injection in one place.
*
* @param int $year
* Comment.
* @param int $month
* Comment.
*
* @return array
* The array holding the field definitions
*/
function bat_date_range_fields($year = NULL, $month = NULL, $granularity = 'bat_hourly') {
$date_range_fields = [];
$config = \Drupal::config('bat.settings');
$date = new DateTime();
if ($year && $month) {
// Calculate min and max dates of the specified year/month.
$date->setDate($year, $month, 01);
$min_date = $date->format($config->get('bat_daily_date_format'));
$date->modify('last day of this month');
$max_date = $date->format($config->get('bat_daily_date_format'));
$extra_attributes = [
'min' => $min_date,
'max' => $max_date,
'bat-min' => $min_date,
'bat-max' => $max_date,
];
}
else {
$date->modify('+' . $config->get('bat_event_start_date') . ' days');
$extra_attributes = [
'min' => $date->format($config->get('bat_daily_date_format')),
'bat-min' => $date->format($config->get('bat_daily_date_format')),
];
}
$date_range_fields['bat_start_date'] = [
'#prefix' => '<div class="form-wrapper bat-date-range"><div class="start-date">',
'#suffix' => '</div>',
'#type' => 'date',
'#title' => t('Event Start'),
'#date_date_format' => $config->get('bat_daily_date_format'),
'#attributes' => $extra_attributes + [
'type' => 'date',
'class' => [
'bat_start_date',
],
],
'#required' => TRUE,
];
$date_range_fields['bat_end_date'] = [
'#prefix' => '<div class="end-date">',
'#suffix' => '</div></div>',
'#type' => 'date',
'#title' => t('Event End'),
'#date_date_format' => $config->get('bat_daily_date_format'),
'#attributes' => $extra_attributes + [
'type' => 'date',
'class' => [
'bat_end_date',
],
],
'#required' => TRUE,
'#attached' => ['library' => ['bat/bat_date_range']],
];
return $date_range_fields;
}
/**
* Prepares variables for Type Group templates.
*
* Default template: bat-type-group.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the user information and any
* - attributes: HTML attributes for the containing element.
*/
function template_preprocess_bat_type_group(array &$variables) {
// Fetch Type Group Entity Object.
$type_group = $variables['elements']['#bat_type_group'];
// Helpful $content variable for templates.
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Fetches a type group object.
*
* @param int $group_id
* Integer specifying the group id.
* @param bool $reset
* A boolean indicating whether the internal cache should be reset.
*
* @return \Drupal\bat\Entity\TypeGroup
* A fully-loaded $type_group object or FALSE if it cannot be loaded.
*
* @see bat_type_group_load_multiple()
*/
function bat_type_group_load($group_id, $reset = FALSE) {
if ($reset) {
\Drupal::entityTypeManager()->getStorage('bat_type_group')->resetCache();
}
return TypeGroup::load($group_id);
}
/**
* Loads multiple units based on certain conditions.
*
* @param array $group_ids
* An array of group IDs.
* @param array $conditions
* An array of conditions to match against the {type_group} table.
* @param bool $reset
* A boolean indicating that the internal cache should be reset.
*
* @return array
* An array of type group objects, indexed by group_id.
*
* @see bat_type_group_load()
*/
function bat_type_group_load_multiple(array $group_ids, array $conditions, $reset = FALSE) {
if ($reset) {
\Drupal::entityTypeManager()->getStorage('bat_type_group')->resetCache();
}
if (!empty($conditions)) {
$query = \Drupal::entityQuery('bat_type_group');
$query->accessCheck(TRUE);
if (!empty($group_ids)) {
$query->condition('id', $group_ids, 'IN');
}
foreach ($conditions as $key => $value) {
$query->condition($key, $value);
}
$group_ids = $query->execute();
}
return TypeGroup::loadMultiple($group_ids);
}
/**
* Saves a type group to the database.
*
* @param \Drupal\bat\Entity\TypeGroup $group
* The TypeGroup object.
*/
function bat_type_group_save(TypeGroup $group) {
return $group->save();
}
/**
* Deletes a type group.
*
* @param \Drupal\bat\Entity\TypeGroup $group
* The TypeGroup object that represents the group to delete.
*/
function bat_type_group_delete(TypeGroup $group) {
$group->delete();
}
/**
* Deletes multiple type groups.
*
* @param array $group_ids
* An array of group IDs.
*/
function bat_type_group_delete_multiple(array $group_ids) {
$groups = TypeGroup::loadMultiple($group_ids);
foreach ($groups as $group) {
$group->delete();
}
}
/**
* Gets an array of all type group bundles, keyed by the bundle name.
*
* @param string $bundle_name
* If set, the bundle with the given name is returned.
* @param bool $reset
* A boolean indicating that the internal cache should be reset.
*
* @return \Drupal\bat\Entity\TypeGroupBundle[]
* Depending whether $bundle isset, an array of type group
* bundles or a single one.
*/
function bat_type_group_get_bundles($bundle_name = NULL, $reset = FALSE) {
if ($reset) {
\Drupal::entityTypeManager()->getStorage('bat_type_group_bundle')->resetCache();
}
$types = TypeGroupBundle::loadMultiple();
return isset($bundle_name) ? $types[$bundle_name] : $types;
}
/**
* Creates a type group object.
*
* @param array $values
* The properties for the new type group bundle.
*/
function bat_type_group_create(array $values) {
return TypeGroup::create($values);
}
/**
* Menu argument loader; Load a type group bundle by string.
*
* @param string $bundle
* The machine-readable name of a type group bundle to load.
* @param bool $reset
* A boolean indicating whether the internal cache should be reset.
*
* @return \Drupal\bat\Entity\TypeGroupBundle
* A type group bundle array or FALSE if $bundle does not exist.
*/
function bat_type_group_bundle_load($bundle, $reset = FALSE) {
if ($reset) {
\Drupal::entityTypeManager()->getStorage('bat_type_group_bundle')->resetCache([$bundle]);
}
return TypeGroupBundle::load($bundle);
}
// from commerce commerce_get_entity_display.
/**
* Gets the entity display for the given entity type and bundle.
*
* The entity display will be created if missing.
*
* @param string $entity_type
* The entity type.
* @param string $bundle
* The bundle.
* @param string $display_context
* The display context ('view' or 'form').
*
* @throws \InvalidArgumentException
* Thrown when an invalid display context is provided.
*
* @return \Drupal\Core\Entity\Display\EntityDisplayInterface
* The entity display.
*/
function bat_get_entity_display($entity_type, $bundle, $display_context) {
if (!in_array($display_context, ['view', 'form'])) {
throw new \InvalidArgumentException(sprintf('Invalid display_context %s passed to ____get_display().', $display_context));
}
$storage = \Drupal::entityTypeManager()->getStorage('entity_' . $display_context . '_display');
$display = $storage->load($entity_type . '.' . $bundle . '.default');
if (!$display) {
$display = $storage->create([
'targetEntityType' => $entity_type,
'bundle' => $bundle,
'mode' => 'default',
'status' => TRUE,
]);
}
return $display;
}
