<?php
/**
* Geolocation functions.
*
* @category Features
* @package My Calendar Pro
* @author Joe Dolson
* @license GPLv3
* @link https://www.joedolson.com/my-calendar-pro/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
require __DIR__ . '/vendor/autoload.php';
use GeoIp2\Database\Reader;
/**
* Get user location by IP.
*/
function mcs_get_user_ip() {
// disabled by default, as required database is proprietary.
if ( ! ( defined( 'MCS_GEOLOCATION_ENABLED' ) && MCS_GEOLOCATION_ENABLED ) ) {
return false;
}
$client = isset( $_SERVER['HTTP_CLIENT_IP'] ) ? sanitize_text_field( $_SERVER['HTTP_CLIENT_IP'] ) : '';
$forward = isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ? sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_FOR'] ) : '';
$remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( $_SERVER['REMOTE_ADDR'] ) : '';
if ( filter_var( $client, FILTER_VALIDATE_IP ) ) {
$ip = $client;
} elseif ( filter_var( $forward, FILTER_VALIDATE_IP ) ) {
$ip = $forward;
} else {
$ip = $remote;
}
return $ip;
}
/**
* Submit a query to recenter geolocation map.
*
* @param array $address Address information.
*
* @return string
*/
function mcs_geolocate_form( $address = array() ) {
$nonce = wp_create_nonce( 'mc-geolocate-nonce' );
// Enqueue Maps scripting.
$api_key = mc_get_option( 'gmap_api_key' );
if ( $api_key ) {
$args = array(
'strategy' => 'async',
);
wp_enqueue_script( 'mc-maps' );
wp_localize_script(
'mc-maps',
'gmaps',
array(
'toggle' => '<span class="dashicons dashicons-arrow-right" aria-hidden="true"></span><span class="screen-reader-text">' . __( 'Location Details', 'my-calendar-pro' ) . '</span>',
)
);
}
$fields = array(
'street' => array(
'label' => __( 'Street Address', 'my-calendar-pro' ),
'auto' => 'address-line1',
),
'city' => array(
'label' => __( 'City', 'my-calendar-pro' ),
'auto' => 'address-level2',
),
'state' => array(
'label' => __( 'State or Province', 'my-calendar-pro' ),
'auto' => 'address-level1',
),
'zip' => array(
'label' => __( 'Postal Code', 'my-calendar-pro' ),
'auto' => 'postal-code',
),
);
$fields = apply_filters( 'mc_geolocate_fields', $fields );
if ( empty( $fields ) ) {
return '';
}
$output = '';
foreach ( $fields as $key => $value ) {
$current = ( ! empty( $address ) && isset( $address[ $key ] ) ) ? $address[ $key ] : '';
if ( isset( $_REQUEST['mc_address'] ) ) {
$address = map_deep( $_REQUEST['mc_address'], 'sanitize_text_field' );
$current = isset( $address[ $key ] ) ? $address[ $key ] : '';
}
$output .= '<p class="mc_field_' . sanitize_html_class( $key ) . '">
<label for="mc_id_' . sanitize_html_class( $key ) . '">' . esc_html( $value['label'] ) . '</label>
<input type="text" autocomplete="' . esc_attr( $value['auto'] ) . '" value="' . $current . '" id="mc_id_' . sanitize_html_class( $key ) . '" name="mc_address[' . sanitize_html_class( $key ) . ']" />
</p>';
}
$form = '
<div class="mc-geolocate-form">
<h2 class="screen-reader-text">' . esc_html__( 'Search Locations', 'my-calendar-pro' ) . '</h2>
<form action="" method="POST">
<input type="hidden" name="mc_gl_nonce" value="' . $nonce . '" />
<div class="mc-gl-fields">
' . $output . '
<p class="mc-submit-button"><button class="button-primary mc-button">' . esc_html__( 'Find Nearby Locations', 'my-calendar-pro' ) . '</button></p>
</div>
</form>
</div>';
return $form;
}
/**
* Get locations proximate to center point.
*
* @param float $latitude Center latitude.
* @param float $longitude Center longitude.
* @param array $address Array of address strings.
* @param int $radius Radius to search in $unit.
* @param int $limit Number of results to display.
* @param int $unit Unit for the `$radius` parameter (miles or kilometers).
*
* @return array
*/
function mcs_geolocate( $latitude = false, $longitude = false, $address = array(), $radius = 100, $limit = 25, $unit = 'kilometers' ) {
global $wpdb;
$form = '';
$output = '';
$text = '';
if ( isset( $_POST['mc_gl_nonce'] ) ) {
$security = wp_verify_nonce( $_POST['mc_gl_nonce'], 'mc-geolocate-nonce' );
if ( $security ) {
$address = isset( $_REQUEST['mc_address'] ) ? map_deep( $_REQUEST['mc_address'], 'sanitize_text_field' ) : $address;
}
}
if ( ! empty( $address ) ) {
$coordinates = mc_get_location_coordinates( false, $address );
$latitude = isset( $coordinates['latitude'] ) ? $coordinates['latitude'] : false;
$longitude = isset( $coordinates['longitude'] ) ? $coordinates['longitude'] : false;
}
if ( ! $latitude || ! $longitude ) {
$ip = mcs_get_user_ip();
if ( $ip ) {
$filepath = plugin_dir_path( __DIR__ ) . 'my-calendar-custom/db/GeoLite2-City.mmdb';
/**
* Filter the location of the GeoLite City database.
*
* @hook mcs_geolocate_db_filepath
*
* @param {string} $filepath Path to file.
*
* @return {string}
*/
$filepath = apply_filters( 'mcs_geolocate_db_filepath', $filepath );
if ( file_exists( $filepath ) ) {
$reader = new Reader( $filepath );
try {
$response = $reader->city( $ip );
$latitude = (float) $response->location->latitude;
$longitude = (float) $response->location->longitude;
} catch ( Exception $e ) {
if ( $e->getMessage() && current_user_can( 'manage_options' ) ) {
$text = $e->getMessage();
}
}
}
} else {
$latitude = 0.000000;
$longitude = 0.000000;
}
}
$display_radius = (int) $radius;
$radius = ( 'kilometers' === $unit ) ? (int) $radius : (int) ( $radius / .62 );
$limit = (int) $limit;
// If units = miles, 3956 & 69; if km = 6371 & 111.0447
// since MySQL 5.7.6; MariaDB 10.2.38; 10.3.29; 10.4.19; 10.5.10;
// When ST Distance Sphere supported; .00062... = miles; .001 = km; default is meters.
$locations = $wpdb->get_results(
$wpdb->prepare(
'SELECT *, ST_Distance_Sphere( point( %f, %f ), point(location_longitude, location_latitude)) * .000621371192 AS distance_in_miles FROM %i HAVING distance_in_miles <= %f ORDER BY distance_in_miles ASC LIMIT %d',
$longitude,
$latitude,
my_calendar_locations_table(),
$radius,
$limit
)
);
if ( empty( $locations ) ) {
// Alternate query if ST Distance Sphere not supported.
$locations = $wpdb->get_results(
$wpdb->prepare(
'SELECT *, 3956 * 2 * ASIN(SQRT( POWER(SIN((%f - location_latitude)*pi()/180/2),2)
+COS(%f*pi()/180 )*COS(location_latitude*pi()/180)*POWER(SIN((%f-location_longitude)*pi()/180/2),2)
)) AS distance FROM %i WHERE location_longitude BETWEEN (%f-%f/cos(radians(%f))*69)
AND (%f+%f/cos(radians(%f))*69) AND location_latitude BETWEEN (%f-(%f/69))
AND (%f+(%f/69)) HAVING distance < %f ORDER BY distance LIMIT %d',
$latitude,
$latitude,
$longitude,
my_calendar_locations_table(),
$longitude,
$radius,
$latitude,
$longitude,
$radius,
$latitude,
$latitude,
$radius,
$latitude,
$radius,
$radius,
$limit
)
);
}
if ( ! empty( $locations ) ) {
// Add location post ID to object.
foreach ( $locations as $key => $location ) {
$location->location_post = mc_get_location_post( $location->location_id, false );
$locations[ $key ] = $location;
}
$form = mcs_geolocate_form( $address );
$count = count( $locations );
$distance = $display_radius;
// translators: 1) number of locations, 2) distance away, 3) units (miles or kilometers).
$text = '<p class="mcs-geolocation-notice">' . esc_html( sprintf( _n( '%1$d location found within %2$d %3$s.', '%1$d locations found within %2$d %3$s.', $count, 'my-calendar-pro' ), $count, $distance, $unit ) ) . '</p>';
$output = mc_generate_map( $locations, 'location', true, true );
}
return '<div class="mc-geolocate-wrapper">' . $form . $text . $output . '</div>';
}
add_shortcode( 'geolocate', 'mcs_geolocation_map' );
/**
* Generate a geolocation map based on latitude/longitude.
*
* @param array $atts Shortcode attributes.
* @param string $content Wrapped shortcode content.
*
* @return string
*/
function mcs_geolocation_map( $atts, $content ) {
$args = shortcode_atts(
array(
'latitude' => '',
'longitude' => '',
'radius' => 100,
'limit' => 25,
'unit' => 'kilometers', // or miles.
),
$atts,
'geolocate'
);
return mcs_geolocate( $args['latitude'], $args['longitude'], array(), $args['radius'], $args['limit'], $args['unit'] );
}