social_geolocation-8.x-1.2/modules/social_geolocation_search/social_geolocation_search.module
modules/social_geolocation_search/social_geolocation_search.module
<?php
/**
* @file
* Contains hook implementations for the Social Geolocation Search module.
*/
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\group\Entity\GroupType;
use Drupal\search_api\Plugin\views\query\SearchApiQuery;
use Drupal\search_api\Query\QueryInterface;
// The factor that is used to convert miles to kilometers.
const SOCIAL_GEOLOCATION_SEARCH_MI_TO_KM = 1.609344;
// The radius of the earth in kilometers.
const SOCIAL_GEOLOCATION_SEARCH_EARTH_RADIUS = 6371;
/**
* Implements hook_help().
*/
function social_geolocation_search_help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.social_geolocation_search':
return 'This module allows you to find events, groups and users by location in search. It works with a SQL and SOLR back-end.';
}
return NULL;
}
/**
* Retrieves the arguments for the search views.
*
* All returned values are normalised to kilometers.
*
* @return array
* An array containing proximity, proximity-lat, proximity-lng or an empty
* array if one of the values was missing.
*/
function _social_geolocation_search_get_proximity_arguments(): array {
$values = [];
$args = \Drupal::request()->query->all();
// Filter out the arguments needed without iterating over a possibly long list
// of arguments.
foreach (['proximity', 'proximity-lat', 'proximity-lng'] as $key) {
// If one of the keys is missing none can be used.
if (empty($args[$key])) {
return [];
}
$values[$key] = $args[$key];
}
// Check whether we use miles or kilometers for our proximity.
$unit_of_measurement = \Drupal::config('social_geolocation.settings')->get('unit_of_measurement');
if ($unit_of_measurement === 'mi') {
$values['proximity'] *= SOCIAL_GEOLOCATION_SEARCH_MI_TO_KM;
}
return $values;
}
/**
* Implements hook_search_api_db_query_alter().
*
* Alters the search API query for the database backend to implement proximity
* searching. This is needed because there seems to be no module that properly
* provides this for a database backend. Additionally actual SQL is needed to
* calculate the distances so this can't be used with other backends.
*/
function social_geolocation_search_api_db_query_alter(SelectInterface $db_query, QueryInterface $query): void {
$args = _social_geolocation_search_get_proximity_arguments();
// If there is no proximity data then there is no alter to perform.
if (empty($args)) {
return;
}
// Get the tables affected by this query.
$tables = $db_query->getTables();
$index = $query->getIndex()->id();
$table = 'search_api_db_' . $index;
// If we have only one table, that's our table.
if (count($tables) === 1) {
$aliases = array_keys($tables);
$alias = reset($aliases);
}
// With multiple tables, check that we're using the index table.
else {
$found = FALSE;
foreach ($tables as $alias => $data) {
if ($data['table'] === $table) {
$found = TRUE;
break;
}
}
// Return if this query does not have the index table (should never happen?)
if (!$found) {
return;
}
}
// Add a lot of access checks for anonymous users.
// https://bitbucket.org/goalgorilla/pachamama/commits/238ea4f08d7caca20cb92f393d9df968fec60a2e
// https://bitbucket.org/goalgorilla/pachamama/commits/d79cd6713333f0a1008924613ab89ab1b6c073cf
if (\Drupal::currentUser()->isAnonymous()) {
$group_types = GroupType::loadMultiple();
$group_type_ids = [];
// Collect the group types that anonymous users are allowed to see.
// (This probably won't work for Flexible groups)
/** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
foreach ($group_types as $group_type_id => $group_type) {
$anonymous_role = \Drupal::entityTypeManager()
->getStorage('group_role')
->load($group_type->id() . '-anonymous');
if ($anonymous_role->hasPermission('view group')) {
$group_type_ids[] = $group_type_id;
}
}
if ($index === 'social_all') {
// Field specifying the index' data source (e.g. entity:group,
// entity:profile, etc.)
$source_field = $alias . '.search_api_datasource';
$entity_type = 'entity:group';
// If anonymous users are allowed to see groups then we limit results
// for groups to be of that group type.
if (!empty($group_type_ids)) {
$and = $db_query->andConditionGroup()
->condition($source_field, $entity_type)
->condition("$alias.group_type", $group_type_ids, 'IN');
$or = $db_query->orConditionGroup()
->condition($source_field, $entity_type, '<>')
->condition($and);
$db_query->condition($or);
}
// If a user is not allowed to see any groups then we remove groups from
// the results altogether.
else {
$db_query->condition($source_field, $entity_type, '<>');
}
}
// For group searches we limit results to group types the AN user is
// allowed to see.
if ($index === 'social_groups') {
if (!empty($group_type_ids)) {
$db_query->condition("$alias.type", $group_type_ids, 'IN');
}
else {
return;
}
}
}
// No idea why we join the base table with itself if it's not there yet.
if (count($tables) == 1 && !is_string($tables[$alias]['table'])) {
$new_alias = $alias . '_base';
$db_query->join($table, $new_alias, "$alias.item_id = $new_alias.item_id");
$alias = $new_alias;
}
$snippet = '';
// Entity types that can have location data for each index.
$entity_types = [
'social_content' => 'node',
'social_groups' => 'group',
'social_users' => 'profile',
];
if ($index === 'social_all') {
$entity_types = array_values($entity_types);
}
else {
$entity_types = [$entity_types[$index]];
}
// Add a complex snippet.
foreach ($entity_types as $id => $entity_type) {
if ($id) {
$snippet .= ') OR (';
}
$snippet .= "( $alias.search_api_datasource = 'entity:$entity_type' ) AND (
( ACOS(LEAST(1,
:filter_latcos
* $alias.{$entity_type}_lat_cos
* COS( :filter_lng - $alias.{$entity_type}_lng_rad )
+
:filter_latsin
* $alias.{$entity_type}_lat_sin
)) * :earth_radius
) < :proximity )";
}
$db_query->where("(( $snippet ))", [
':filter_latcos' => cos(deg2rad($args['proximity-lat'])),
':filter_lng' => deg2rad($args['proximity-lng']),
':filter_latsin' => sin(deg2rad($args['proximity-lat'])),
':earth_radius' => SOCIAL_GEOLOCATION_SEARCH_EARTH_RADIUS,
':proximity' => $args['proximity'],
]);
}
/**
* Alternative to the search_api_solr alter hook.
*
* This method is called by social_geolocation_search_views_query_alter().
*
* Alters the search API query for the SOLR backend to provide location based
* searching. This is done to complement the databae query alter. It's performed
* here instead of in a contrib module so we can work with the existing fields.
*/
function _social_geolocation_search_alter_solr_query(SearchApiQuery $query): void {
$args = _social_geolocation_search_get_proximity_arguments();
// If there is no proximity data then there is no alter to perform.
if (empty($args)) {
return;
}
$datasource_ids = $query->getIndex()->getDatasourceIds();
// Location searches can't be performed for multiple entity types at the same
// time.
if (count($datasource_ids) > 1) {
return;
}
// The datasource is specified as type:type_id.
// e.g. entity:profile for profile entities.
$datasource = explode(':', $datasource_ids[0]);
[$datasource_type, $entity_type] = $datasource;
// Only entity based searches are supported.
if ($datasource_type !== 'entity') {
return;
}
$location_options = $query->getOption('search_api_location', []);
$location_options[] = [
// The field as known to our backend.
'field' => "{$entity_type}_geolocation",
// The search latitude and longitude.
'lat' => $args['proximity-lat'],
'lon' => $args['proximity-lng'],
// The search radius.
'radius' => $args['proximity'],
];
$query->setOption('search_api_location', $location_options);
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Enhance the Views exposed filter blocks forms.
*/
function social_geolocation_search_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
$filter_forms = [
'views-exposed-form-search-all-page',
'views-exposed-form-search-all-page-no-value',
'views-exposed-form-search-content-page',
'views-exposed-form-search-content-page-no-value',
'views-exposed-form-search-users-page',
'views-exposed-form-search-users-page-no-value',
'views-exposed-form-search-groups-page',
'views-exposed-form-search-groups-page-no-value',
];
if (!in_array($form['#id'], $filter_forms, TRUE)) {
return;
}
social_geolocation_attach_views_location_filter($form);
// On the content search page the location filtering is only available when
// the user is searching for events specifically as other node types don't
// have location data at this moment.
if (isset($form['location_details']) && in_array($form['#id'], ['views-exposed-form-search-content-page', 'views-exposed-form-search-content-page-no-value'], TRUE)) {
$form['location_details']['#states'] = [
'visible' => [
':input[name=type]' => [
'value' => 'event',
],
],
];
}
}
/**
* Implements hook_block_view_BASE_BLOCK_ID_alter().
*
* Enhance the Views exposed filter blocks.
*/
function social_geolocation_search_block_view_views_exposed_filter_block_alter(array &$build, BlockPluginInterface $block): void {
$filter_blocks = [
'search_all-page',
'search_groups-page',
];
if (in_array($build['#derivative_plugin_id'], $filter_blocks)) {
// Disable cache for exposed filter block to get correct current path,
// which is used in $form['#action'].
$build['#cache'] = [
'max-age' => 0,
];
}
}
