Code for Recovery is a nonprofit organization of volunteer members building technology services for recovery fellowships, such as A.A. and Al-Anon.', '12-step-meeting-list'), TSML_ALLOWED_HTML) ?>
';
foreach ($meetings as $m) {
$output .= '';
$output .= '' . tsml_format_day_and_time($m['day'], $m['time'], ' ', true) . ' ';
if ($m['id'] != $meeting_id) {
$output .= '' . $m['name'] . ' ';
} else {
$output .= $m['name'];
}
$output .= ' ';
}
$output .= '';
echo wp_kses($output, ['ol' => [], 'li' => [], 'a' => ['href' => []], 'span' => [], 'time' => []]);
}
/**
* add an admin screen update message
* used: tsml_import() and admin_types.php
*
* @param mixed $message
* @param mixed $type can be success, warning, info, or error
* @return void
*/
function tsml_alert($message, $type = 'success')
{
echo '' . wp_kses($message, TSML_ALLOWED_HTML) . '
';
}
/**
* enqueue assets for public or admin page
* used: in templates and on admin_edit.php
*
* @return void
*/
function tsml_assets()
{
global $tsml_map, $tsml_street_only, $tsml_programs, $tsml_strings, $tsml_program,
$tsml_distance_units, $tsml_defaults, $tsml_columns, $tsml_nonce, $tsml_debug;
wp_enqueue_script('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true);
wp_enqueue_style('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], '1.9.4');
if (is_admin()) {
// dashboard page assets
wp_enqueue_style('tsml_admin', plugins_url('../assets/css/admin.min.css', __FILE__), [], TSML_VERSION);
wp_enqueue_script('tsml_admin', plugins_url('../assets/js/admin.min.js', __FILE__), ['jquery'], TSML_VERSION, true);
wp_localize_script('tsml_admin', 'tsml', [
'ajaxurl' => admin_url('admin-ajax.php'),
'debug' => WP_DEBUG,
'tsml_debug' => !!$tsml_debug,
'map' => $tsml_map,
'nonce' => wp_create_nonce($tsml_nonce),
]);
} else {
// public page assets
global $post;
wp_enqueue_style('tsml_public', plugins_url('../assets/css/public.min.css', __FILE__), [], TSML_VERSION);
wp_enqueue_script('jquery_validate', plugins_url('../assets/js/jquery.validate.min.js', __FILE__), ['jquery'], TSML_VERSION, true);
wp_enqueue_script('tsml_public', plugins_url('../assets/js/public.min.js', __FILE__), ['jquery'], TSML_VERSION, true);
wp_enqueue_script('jquery-ui-autocomplete');
wp_localize_script('tsml_public', 'tsml', [
'ajaxurl' => admin_url('admin-ajax.php'),
'columns' => array_keys($tsml_columns),
'days' => [
__('Sunday', '12-step-meeting-list'),
__('Monday', '12-step-meeting-list'),
__('Tuesday', '12-step-meeting-list'),
__('Wednesday', '12-step-meeting-list'),
__('Thursday', '12-step-meeting-list'),
__('Friday', '12-step-meeting-list'),
__('Saturday', '12-step-meeting-list'),
],
'debug' => WP_DEBUG,
'defaults' => $tsml_defaults,
'distance_units' => $tsml_distance_units,
'flags' => $tsml_programs[$tsml_program]['flags'],
'map' => $tsml_map,
'nonce' => wp_create_nonce($tsml_nonce),
'program' => empty($tsml_programs[$tsml_program]['abbr']) ? $tsml_programs[$tsml_program]['name'] : $tsml_programs[$tsml_program]['abbr'],
'street_only' => $tsml_street_only,
'strings' => $tsml_strings,
'types' => empty($tsml_programs[$tsml_program]['types']) ? [] : $tsml_programs[$tsml_program]['types'],
'meeting_id' => isset($post->ID) ? $post->ID : '',
]);
}
}
/**
* set geo boundaries from current data (for biased geocoding)
*
* @return void
*/
function tsml_bounds()
{
global $wpdb, $tsml_bounds;
// get north & south
$latitudes = $wpdb->get_row('SELECT
MAX(m.meta_value) north,
MIN(m.meta_value) south
FROM ' . $wpdb->postmeta . ' m
JOIN ' . $wpdb->posts . ' p ON p.ID = m.post_id
WHERE m.meta_key = "latitude" AND p.post_type = "tsml_location"');
// get east & west
$longitudes = $wpdb->get_row('SELECT
MAX(m.meta_value) west,
MIN(m.meta_value) east
FROM ' . $wpdb->postmeta . ' m
JOIN ' . $wpdb->posts . ' p ON p.ID = m.post_id
WHERE m.meta_key = "longitude" AND p.post_type = "tsml_location"');
// if results, get bounding box and cache it
if ($latitudes && $longitudes) {
// add 25% margin to the bounds
$width = ($longitudes->east - $longitudes->west) / 25;
$height = ($latitudes->north - $latitudes->south) / 25;
$tsml_bounds = [
'north' => $latitudes->north + $height,
'east' => $longitudes->east + $width,
'south' => $latitudes->south - $height,
'west' => $longitudes->west - $width,
];
update_option('tsml_bounds', $tsml_bounds);
}
}
/**
* try to build a cache of meetings to help with CPU load
*
* @return void
*/
function tsml_cache_rebuild()
{
// flush wp object cache
wp_cache_flush();
// rebuild TSML meeting cache file
tsml_get_meetings([], false);
}
/**
* calculate attendance option given types and address
* called in tsml_get_meetings()
*
* @param mixed $types
* @param mixed $approximate
* @return string
*/
function tsml_calculate_attendance_option($types, $approximate)
{
$attendance_option = '';
// Handle when the types list is empty, this prevents PHP warnings
if (empty($types)) {
$types = [];
}
if (in_array('TC', $types) && in_array('ONL', $types)) {
// Types has both Location Temporarily Closed and Online, which means it should be an online meeting
$attendance_option = 'online';
} elseif (in_array('TC', $types)) {
// Types has Location Temporarily Closed, but not online, which means it really is temporarily closed
$attendance_option = 'inactive';
} elseif (in_array('ONL', $types)) {
// Types has Online, but not Temp closed, which means it's a hybrid (or online)
$attendance_option = 'hybrid';
if ($approximate == 'yes') {
$attendance_option = 'online';
}
} else {
// Neither Online or Temp Closed, which means it's in person (or inactive)
$attendance_option = 'in_person';
if ($approximate == 'yes') {
$attendance_option = 'inactive';
}
}
return $attendance_option;
}
/**
* Render svg icon icon
*
* @param string $icon cash|directions|email|link|phone
* @return void
*/
function tsml_icon($icon)
{ ?>
[
'name' => __('Regions', '12-step-meeting-list'),
'singular_name' => __('Region', '12-step-meeting-list'),
'menu_name' => __('Regions', '12-step-meeting-list'),
'all_items' => __('All Regions', '12-step-meeting-list'),
'edit_item' => __('Edit Region', '12-step-meeting-list'),
'view_item' => __('View Region', '12-step-meeting-list'),
'update_item' => __('Update Region', '12-step-meeting-list'),
'add_new_item' => __('Add New Region', '12-step-meeting-list'),
'new_item_name' => __('New Region', '12-step-meeting-list'),
'parent_item' => __('Parent Region', '12-step-meeting-list'),
'parent_item_colon' => __('Parent Region:', '12-step-meeting-list'),
'search_items' => __('Search Regions', '12-step-meeting-list'),
'popular_items' => __('Popular Regions', '12-step-meeting-list'),
'not_found' => __('No regions found.', '12-step-meeting-list'),
],
'hierarchical' => true,
'public' => false,
'show_ui' => true,
]);
register_taxonomy('tsml_district', 'tsml_group', [
'labels' => [
'name' => __('District', '12-step-meeting-list'),
'singular_name' => __('District', '12-step-meeting-list'),
'menu_name' => __('District', '12-step-meeting-list'),
'all_items' => __('All Districts', '12-step-meeting-list'),
'edit_item' => __('Edit District', '12-step-meeting-list'),
'view_item' => __('View District', '12-step-meeting-list'),
'update_item' => __('Update District', '12-step-meeting-list'),
'add_new_item' => __('Add New District', '12-step-meeting-list'),
'new_item_name' => __('New District', '12-step-meeting-list'),
'parent_item' => __('Parent Area', '12-step-meeting-list'),
'parent_item_colon' => __('Parent Area:', '12-step-meeting-list'),
'search_items' => __('Search Districts', '12-step-meeting-list'),
'popular_items' => __('Popular Districts', '12-step-meeting-list'),
'not_found' => __('No districts found.', '12-step-meeting-list'),
],
'hierarchical' => true,
'public' => false,
'show_ui' => true,
]);
register_post_type(
'tsml_meeting',
[
'labels' => [
'name' => __('Meetings', '12-step-meeting-list'),
'singular_name' => __('Meeting', '12-step-meeting-list'),
'not_found' => __('No meetings added yet.', '12-step-meeting-list'),
'add_new_item' => __('Add New Meeting', '12-step-meeting-list'),
'search_items' => __('Search Meetings', '12-step-meeting-list'),
'edit_item' => __('Edit Meeting', '12-step-meeting-list'),
'view_item' => __('View Meeting', '12-step-meeting-list'),
],
'supports' => ['title', 'author'],
'public' => $is_public,
'show_ui' => true,
'has_archive' => $is_public,
'menu_icon' => 'dashicons-groups',
'rewrite' => ['slug' => $tsml_slug, 'with_front' => apply_filters('tsml_meeting_with_front', true)],
]
);
register_post_type(
'tsml_location',
[
'labels' => [
'name' => __('Locations', '12-step-meeting-list'),
'singular_name' => __('Location', '12-step-meeting-list'),
'menu_name' => __('Locations', '12-step-meeting-list'),
'all_items' => __('All Locations', '12-step-meeting-list'),
'edit_item' => __('Edit Location', '12-step-meeting-list'),
'view_item' => __('View Location', '12-step-meeting-list'),
'update_item' => __('Update Location', '12-step-meeting-list'),
'add_new_item' => __('Add New Location', '12-step-meeting-list'),
'new_item_name' => __('New Location', '12-step-meeting-list'),
'parent_item' => __('Parent Location', '12-step-meeting-list'),
'parent_item_colon' => __('Parent Location:', '12-step-meeting-list'),
'search_items' => __('Search Locations', '12-step-meeting-list'),
'popular_items' => __('Popular Locations', '12-step-meeting-list'),
'not_found' => __('No locations found.', '12-step-meeting-list'),
],
'supports' => ['title'],
'public' => $is_public,
'show_ui' => false,
'has_archive' => $is_public,
'capabilities' => ['create_posts' => false],
'rewrite' => ['slug' => 'locations'],
'taxonomies' => ['tsml_region'],
]
);
register_post_type(
'tsml_group',
[
'labels' => [
'name' => __('Groups', '12-step-meeting-list'),
'singular_name' => __('Group', '12-step-meeting-list'),
'menu_name' => __('Groups', '12-step-meeting-list'),
'all_items' => __('All Groups', '12-step-meeting-list'),
'edit_item' => __('Edit Group', '12-step-meeting-list'),
'view_item' => __('View Group', '12-step-meeting-list'),
'update_item' => __('Update Group', '12-step-meeting-list'),
'add_new_item' => __('Add New Group', '12-step-meeting-list'),
'new_item_name' => __('New Group', '12-step-meeting-list'),
'parent_item' => __('Parent Group', '12-step-meeting-list'),
'parent_item_colon' => __('Parent Group:', '12-step-meeting-list'),
'search_items' => __('Search Groups', '12-step-meeting-list'),
'popular_items' => __('Popular Groups', '12-step-meeting-list'),
'not_found' => __('No groups found.', '12-step-meeting-list'),
],
'supports' => ['title'],
'public' => true,
'show_ui' => false,
'has_archive' => false,
'capabilities' => ['create_posts' => false],
]
);
}
/**
* define custom meeting types for your area
* used: theme's functions.php
*
* @param mixed $types
* @return void
*/
function tsml_custom_types($types)
{
add_action('init', function () use ($types) {
global $tsml_programs, $tsml_program;
foreach ($types as $key => $value) {
if (empty($value)) {
// if empty, remove existing type
unset($tsml_programs[$tsml_program]['types'][$key]);
continue;
}
$tsml_programs[$tsml_program]['types'][$key] = $value;
}
asort($tsml_programs[$tsml_program]['types']);
});
}
/**
* function used for debugging
*
* @param mixed $obj
* @return never
*/
function tsml_dd($obj)
{
echo '';
print_r($obj);
echo ' ';
exit;
}
/**
* efficiently remove an array of post_ids
* used: tsml_delete_orphans(), admin-import.php
*
* @param mixed $post_ids
* @return void
*/
function tsml_delete($post_ids)
{
global $wpdb;
//special case
if ($post_ids == 'everything') {
$post_ids = get_posts([
'post_type' => ['tsml_meeting', 'tsml_location', 'tsml_group'],
'post_status' => 'any',
'fields' => 'ids',
'numberposts' => -1,
]);
//when we're deleting *everything*, also delete regions & districts
if ($term_ids = implode(',', $wpdb->get_col('SELECT term_id FROM ' . $wpdb->term_taxonomy . ' WHERE taxonomy IN ("tsml_district", "tsml_region")'))) {
$wpdb->query('DELETE FROM ' . $wpdb->terms . ' WHERE term_id IN (' . $term_ids . ')');
$wpdb->query('DELETE FROM ' . $wpdb->term_taxonomy . ' WHERE term_id IN (' . $term_ids . ')');
}
}
if (empty($post_ids) || !is_array($post_ids)) {
return;
}
// sanitize
$post_ids = array_map('intval', $post_ids);
$post_ids = array_unique($post_ids);
$post_ids = implode(', ', $post_ids);
// run deletes
$wpdb->query('DELETE FROM ' . $wpdb->posts . ' WHERE ID IN (' . $post_ids . ')');
$wpdb->query('DELETE FROM ' . $wpdb->postmeta . ' WHERE post_id IN (' . $post_ids . ')');
$wpdb->query('DELETE FROM ' . $wpdb->term_relationships . ' WHERE object_id IN (' . $post_ids . ')');
// rebuild cache
tsml_cache_rebuild();
}
/**
* efficiently deletes all orphaned locations and groups (have no meetings associated)
* used: save_post filter
*
* @return void
*/
function tsml_delete_orphans()
{
global $wpdb;
$location_ids = $wpdb->get_col('SELECT l.ID FROM ' . $wpdb->posts . ' l WHERE l.post_type = "tsml_location" AND (SELECT COUNT(*) FROM ' . $wpdb->posts . ' m WHERE m.post_type="tsml_meeting" AND m.post_parent = l.id) = 0');
$group_ids = $wpdb->get_col('SELECT g.ID FROM ' . $wpdb->posts . ' g WHERE g.post_type = "tsml_group" AND (SELECT COUNT(*) FROM ' . $wpdb->postmeta . ' m WHERE m.meta_key="group_id" AND m.meta_value = g.id) = 0');
tsml_delete(array_merge($location_ids, $group_ids));
// edge case: draft-ify locations with only unpublished meetings
$location_ids = $wpdb->get_col('SELECT l.ID FROM ' . $wpdb->posts . ' l
WHERE l.post_type = "tsml_location" AND
(SELECT COUNT(*) FROM ' . $wpdb->posts . ' m
WHERE m.post_type="tsml_meeting" AND m.post_status="publish" AND m.post_parent = l.id) = 0');
if (count($location_ids)) {
$wpdb->query('UPDATE ' . $wpdb->posts . ' l SET l.post_status = "draft" WHERE ID IN (' . implode(', ', $location_ids) . ')');
}
}
/**
* send a nice-looking email
* used by tsml_ajax_feedback() and save.php (change notifications)
*
* @param mixed $to
* @param mixed $subject
* @param mixed $message
* @param mixed $reply_to
* @return bool
*/
function tsml_email($to, $subject, $message, $reply_to = false)
{
$headers = ['Content-Type: text/html; charset=UTF-8'];
if ($reply_to) {
$headers[] = 'Reply-To: ' . $reply_to;
}
// prepend subject as h1
$message = '' . $subject . ' ' . $message;
// inline styles where necessary
$message = str_replace('', '', $message);
$message = str_replace(' ', ' ', $message);
$message = str_replace(' ', '
', $message);
$message = str_replace('
' . $subject . '
';
return wp_mail($to, '[' . get_bloginfo('name') . '] ' . $subject, $message, $headers);
}
/**
* display meeting list on home page (must be set to a static page)
* used: by themes that want it, such as https://github.com/code4recovery/one-page-meeting-list
*
* @param mixed $wp_query
* @return void
*/
function tsml_front_page($wp_query)
{
if (is_admin()) {
return; //don't do this to inside pages
}
if ($wp_query->get('page_id') == get_option('page_on_front')) {
$wp_query->set('post_type', 'tsml_meeting');
$wp_query->set('page_id', '');
$wp_query->is_page = 0;
$wp_query->is_singular = 0;
$wp_query->is_post_type_archive = 1;
$wp_query->is_archive = 1;
}
}
/**
* request accurate address information from google
* used: tsml_ajax_import(), tsml_ajax_geocode()
*
* @param mixed $address
* @return mixed
*/
function tsml_geocode($address)
{
global $tsml_google_overrides;
$address = stripslashes($address);
// check overrides first before anything
if (array_key_exists($address, $tsml_google_overrides)) {
if (empty($tsml_google_overrides[$address]['approximate'])) {
$tsml_google_overrides[$address]['approximate'] = 'no';
}
$tsml_google_overrides[$address]['status'] = 'override';
return $tsml_google_overrides[$address];
}
// check cache
$addresses = tsml_get_option_array('tsml_addresses');
// if key exists && approximate is set for that address, return it
if (array_key_exists($address, $addresses) && !empty($addresses[$address]['approximate'])) {
$addresses[$address]['status'] = 'cache';
return $addresses[$address];
}
$response = tsml_geocode_google($address);
// Return if the status is error
if ($response['status'] == 'error') {
return $response;
}
// cache result
$addresses[$address] = $response;
$addresses[$response['formatted_address']] = $response;
update_option('tsml_addresses', $addresses);
return $response;
}
/**
* call Google for geocoding of the address
*
* @param mixed $address
* @return mixed
*/
function tsml_geocode_google($address)
{
global $tsml_curl_handle, $tsml_language, $tsml_google_overrides, $tsml_bounds;
// Can't Geocode an empty address
if (empty($address)) {
return [
'status' => 'error',
'reason' => 'Addres string was empty',
];
}
// initialize curl handle if necessary
if (!$tsml_curl_handle) {
$tsml_curl_handle = curl_init();
curl_setopt_array($tsml_curl_handle, [
CURLOPT_HEADER => 0,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_SSL_VERIFYPEER => false,
]);
}
// form geocoding request url
$options = [
'application' => 'tsml',
'language' => $tsml_language,
'referrer' => tsml_meetings_url(),
'search' => $address,
];
if (!empty($tsml_bounds) && is_array($tsml_bounds)) {
$options = array_merge($options, $tsml_bounds);
}
$url = TSML_GEOCODING_URL . '/api/geocode?' . http_build_query($options);
// send request to google
curl_setopt($tsml_curl_handle, CURLOPT_URL, $url);
curl_setopt($tsml_curl_handle, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($tsml_curl_handle);
// could not connect error
if ($result === false) {
$error = curl_error($tsml_curl_handle);
tsml_log('geocode_connection_error', $error, $address);
return [
'status' => 'error',
'reason' => 'Google could not validate the address ' . $address . '. Response was ' . $error . '',
];
}
// decode result
$data = json_decode($result);
// if over query limit, wait two seconds and retry, or then exit
if ($data->status === 'OVER_QUERY_LIMIT') {
sleep(2);
$result = curl_exec($tsml_curl_handle);
// could not connect error
if ($result === false) {
return [
'status' => 'error',
'reason' => 'Google could not validate the address ' . $address . '. Response was ' . curl_error($tsml_curl_handle) . '',
];
}
// decode result
$data = json_decode($result);
// if we're still over the limit, stop
if ($data->status === 'OVER_QUERY_LIMIT') {
tsml_log('geocode_error', 'OVER_QUERY_LIMIT', $address);
return [
'status' => 'error',
'reason' => 'We are over the rate limit for the Google Geocoding API.'
];
}
}
// if there are no results report it
if ($data->status === 'ZERO_RESULTS') {
tsml_log('geocode_error', 'ZERO_RESULTS', $address);
return [
'status' => 'error',
'reason' => 'Google could not validate the address ' . $address . '',
];
}
// if result is otherwise bad, stop
if (($data->status !== 'OK') || empty($data->results[0]->formatted_address)) {
tsml_log('geocode_error', $data->status, $address);
return [
'status' => 'error',
'reason' => 'Google gave an unexpected response for address ' . $address . '. Response was ' . var_export($data, true) . ' ',
];
}
// check our overrides array again in case google is wrong
if (array_key_exists($data->results[0]->formatted_address, $tsml_google_overrides)) {
$response = $tsml_google_overrides[$data->results[0]->formatted_address];
if (empty($response['approximate'])) {
$response['approximate'] = 'no';
}
} else {
tsml_log('geocode_success', $data->results[0]->formatted_address, $address);
// start building response
$response = [
'formatted_address' => $data->results[0]->formatted_address,
'latitude' => $data->results[0]->geometry->location->lat,
'longitude' => $data->results[0]->geometry->location->lng,
'approximate' => ($data->results[0]->geometry->location_type === 'APPROXIMATE') ? 'yes' : 'no',
'city' => null,
'status' => 'geocode',
];
// get city, we might need it for the region, and we are going to cache it
foreach ($data->results[0]->address_components as $component) {
if (in_array('locality', $component->types)) {
$response['city'] = $component->short_name;
}
}
}
return $response;
}
/**
* return spelled-out meeting types
* called from save.php (updates) and archive-meetings.php (display)
*
* @param mixed $types
* @return string | void
*/
function tsml_meeting_types($types)
{
global $tsml_programs, $tsml_program;
if (empty($tsml_programs[$tsml_program]['types'])) {
return;
}
$return = [];
foreach ($types as $type) {
if (array_key_exists($type, $tsml_programs[$tsml_program]['types'])) {
$return[] = $tsml_programs[$tsml_program]['types'][$type];
}
}
sort($return);
return implode(', ', $return);
}
/**
* return an array of languages, with legacy code substitutions per-program
* used: variables.php, meeting-edit.php
*/
function tsml_languages($types = [])
{
global $tsml_program;
// https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
$languages = [
'AF' => __('Afrikaans', '12-step-meeting-list'),
'AM' => __('Amharic', '12-step-meeting-list'),
'AR' => __('Arabic', '12-step-meeting-list'),
'BG' => __('Bulgarian', '12-step-meeting-list'),
'DA' => __('Danish', '12-step-meeting-list'),
'DE' => __('German', '12-step-meeting-list'),
'EL' => __('Greek', '12-step-meeting-list'),
'EN' => __('English', '12-step-meeting-list'),
'ES' => __('Spanish', '12-step-meeting-list'),
'FA' => __('Persian', '12-step-meeting-list'),
'FI' => __('Finnish', '12-step-meeting-list'),
'FR' => __('French', '12-step-meeting-list'),
'HE' => __('Hebrew', '12-step-meeting-list'),
'HI' => __('Hindi', '12-step-meeting-list'),
'HR' => __('Croatian', '12-step-meeting-list'),
'HU' => __('Hungarian', '12-step-meeting-list'),
'IS' => __('Icelandic', '12-step-meeting-list'),
'IT' => __('Italian', '12-step-meeting-list'),
'JA' => __('Japanese', '12-step-meeting-list'),
'KA' => __('Georgian', '12-step-meeting-list'),
'KO' => __('Korean', '12-step-meeting-list'),
'LT' => __('Lithuanian', '12-step-meeting-list'),
'ML' => __('Malayalam', '12-step-meeting-list'),
'MT' => __('Maltese', '12-step-meeting-list'),
'NE' => __('Nepali', '12-step-meeting-list'),
'NL' => __('Dutch', '12-step-meeting-list'),
'NO' => __('Norwegian', '12-step-meeting-list'),
'PA' => __('Punjabi', '12-step-meeting-list'),
'PL' => __('Polish', '12-step-meeting-list'),
'PT' => __('Portuguese', '12-step-meeting-list'),
'RU' => __('Russian', '12-step-meeting-list'),
'SK' => __('Slovak', '12-step-meeting-list'),
'SL' => __('Slovenian', '12-step-meeting-list'),
'SV' => __('Swedish', '12-step-meeting-list'),
'TH' => __('Thai', '12-step-meeting-list'),
'TL' => __('Tagalog', '12-step-meeting-list'),
'TR' => __('Turkish', '12-step-meeting-list'),
'UK' => __('Ukrainian', '12-step-meeting-list'),
];
// substitutions
$substitutions = [
'aca' => [
'ES' => 'SP',
],
'al-anon' => [
'ES' => 'S',
],
'aa' => [
'ES' => 'S',
'IT' => 'ITA',
'KO' => 'KOR',
'PA' => 'PUN',
'PL' => 'POL',
'PT' => 'POR',
'RU' => 'RUS',
'TR' => 'TUR',
],
'ca' => [
'ES' => 'S',
'IT' => 'ITA',
'KO' => 'KOR',
'PA' => 'PUN',
'PL' => 'POL',
'PT' => 'POR',
'RU' => 'RUS',
],
'cma' => [
'ES' => 'S',
],
'coda' => [
'ES' => 'S',
],
'eda' => [
'ES' => 'S',
'IT' => 'ITA',
],
'ga' => [
'ES' => 'S',
'IT' => 'ITA',
'KO' => 'KOR',
'PA' => 'PUN',
'PL' => 'POL',
'PT' => 'POR',
'RU' => 'RUS',
],
'slaa' => [
'ES' => 'S',
],
'sia' => [
'ES' => 'S',
'IT' => 'ITA',
'KO' => 'KOR',
'PA' => 'PUN',
'PL' => 'POL',
'PT' => 'POR',
'RU' => 'RUS',
'UK' => 'UA',
],
];
// if the program has no types, return the languages
if (array_key_exists($tsml_program, $substitutions)) {
foreach ($substitutions[$tsml_program] as $key => $value) {
if (array_key_exists($key, $languages)) {
$languages[$value] = $languages[$key];
unset($languages[$key]);
}
}
}
foreach ($languages as $key => $value) {
$types[$key] = $value;
}
asort($types);
return $types;
}
/**
* Render an anchor tag with a class
* @deprecated this function is no longer used. it will be removed in the future
* @param mixed $url
* @param mixed $string
* @param mixed $exclude
* @param mixed $class
* @return string
*/
function tsml_link($url, $string, $exclude = '', $class = false)
{
$appends = $_GET;
if (array_key_exists($exclude, $appends)) {
unset($appends[$exclude]);
}
if (!empty($appends)) {
$url .= strstr($url, '?') ? '&' : '?';
$url .= http_build_query($appends, '', '&');
}
$return = '';
return $return;
}
/**
* return string link with current query string appended
*
* @param string $url
* @param mixed $exclude
* @return string
*/
function tsml_link_url($url, $exclude = '')
{
$appends = $_GET;
if (array_key_exists($exclude, $appends)) {
unset($appends[$exclude]);
}
if (!empty($appends)) {
$url .= strstr($url, '?') ? '&' : '?';
$url .= http_build_query($appends, '', '&');
}
return $url;
}
/**
* link to meetings page with parameters
* used: admin_import.php, admin_settings.php, archive-meetings.php, init.php, widgets.php, tsml_geocode_google()
*
* @param mixed $parameters
* @return string
*/
function tsml_meetings_url($parameters = [])
{
$url = get_post_type_archive_link('tsml_meeting');
if (empty($url)) {
$url = get_home_url();
}
$url = add_query_arg($parameters, $url);
return $url;
}
/**
* convert line breaks in plain text to HTML paragraphs
* used: save.php in lieu of nl2br()
*
* @param mixed $string
* @return string
*/
function tsml_paragraphs($string)
{
$paragraphs = '';
foreach (explode("\n", trim($string)) as $line) {
if ($line = trim($line)) {
$paragraphs .= '' . $line . '
';
}
}
return $paragraphs;
}
/**
* boolean whether current program has types
* used: meeting edit screen, meeting save
*
* @return bool
*/
function tsml_program_has_types()
{
global $tsml_programs, $tsml_program;
return !empty($tsml_programs[$tsml_program]['types']);
}
/**
* exit if user does not have permission to edit meetings
*
* @return void
*/
function tsml_require_meetings_permission()
{
if (!current_user_can(TSML_MEETINGS_PERMISSION)) {
// translators: %s is the permission required
wp_die(wp_kses(sprintf(__('You do not have sufficient permissions to access this page (%s).', '12-step-meeting-list'), TSML_MEETINGS_PERMISSION), TSML_ALLOWED_HTML));
}
}
/**
* exit if user does not have permission to edit settings
*
* @return void
*/
function tsml_require_settings_permission()
{
if (!current_user_can(TSML_SETTINGS_PERMISSION)) {
// translators: %s is the permission required
wp_die(wp_kses(sprintf(__('You do not have sufficient permissions to access this page (%s).', '12-step-meeting-list'), TSML_SETTINGS_PERMISSION), TSML_ALLOWED_HTML));
}
}
/**
* set an option with the currently-used types
* used tsml_import() and save.php
*
* @return void
*/
function tsml_update_types_in_use()
{
global $tsml_types_in_use, $wpdb;
// shortcut to getting all meta values without getting all posts first
$types = $wpdb->get_col('SELECT
m.meta_value
FROM ' . $wpdb->postmeta . ' m
JOIN ' . $wpdb->posts . ' p ON m.post_id = p.id
WHERE p.post_type = "tsml_meeting" AND m.meta_key = "types" AND p.post_status = "publish"');
// main array
$all_types = [];
// loop through results and append to main array
foreach ($types as $type) {
$type = unserialize($type);
if (is_array($type)) {
$all_types = array_merge($all_types, $type);
}
}
// update global variable
$tsml_types_in_use = array_unique($all_types);
// set option value
update_option('tsml_types_in_use', $tsml_types_in_use);
}
/**
* sanitize a value
* used: save.php
*
* @param mixed $type
* @param mixed $value
* @return array|string|null
*/
function tsml_sanitize($type, $value)
{
if ($type == 'url') {
return esc_url_raw($value, ['http', 'https']);
} elseif ($type == 'date') {
return date('Y-m-d', strtotime($value));
} elseif ($type == 'time') {
return date('H:i', strtotime($value));
} elseif ($type == 'phone') {
return preg_replace('/[^0-9,+#]/', '', $value);
}
return sanitize_text_field($value);
}
/**
* sanitize multi-line text
* used tsml_import() and save.php
* @param string $value
* @return string
*/
function tsml_sanitize_text_area($value)
{
return implode("\n", array_map('sanitize_text_field', explode("\n", trim($value))));
}
/**
* does a string end with another string
* used: save.php
*
* @param mixed $string
* @param mixed $end
* @return bool
*/
function tsml_string_ends($string, $end)
{
$length = strlen($end);
if (!$length) {
return true;
}
return (substr($string, -$length) === $end);
}
/**
* tokenize string for the typeaheads
* used: ajax functions
* @param mixed $string
* @return array
*/
function tsml_string_tokens($string)
{
// shorten words that have quotes in them instead of splitting them
$string = html_entity_decode($string);
$string = str_replace("'", '', $string);
$string = str_replace('’', '', $string);
// remove everything that's not a letter or a number
$string = preg_replace("/[^a-zA-Z 0-9]+/", ' ', $string);
// return array
return array_values(array_unique(array_filter(explode(' ', $string))));
}
/**
* Implodes given array $types into lower-cased class names, prefixing each with $prefix
*
* @param string[] $types
* @param string $prefix
* @return string
*/
function tsml_to_css_classes($types, $prefix = 'type-')
{
if (!$types) {
return '';
}
$types = array_map('strtolower', $types);
return $prefix . implode(' ' . $prefix, $types);
}
/**
* Sanitizes a string for sorting purposes. Similar to sanitize_title(), but uses Unicode regular expressions to support multiple languages.
*
* @param string $string
* @return string
*/
function tsml_sanitize_data_sort($string)
{
global $tsml_sanitize_data_sort_regexps;
// Populate regex array only once
if (!isset($tsml_sanitize_data_sort_regexps)) {
$tsml_sanitize_data_sort_regexps = [
['/<[^>]+>/', ''], // Strip HTML Tags
['/[&\'"<>]+/', ''], // Strip unsupported chars
['/[\\/\\.\\p{Zs}\\p{Pd}]/u', '-'], // Change forward slashes, periods, spaces and dashes to dash (Unicode)
['/[^\\p{L}\\p{N}\\p{M}\-]+/u', ''], // Remove any Unicode char that is not an alpha-numeric, mark character or dash
['/\-+/', '-'], // Convert runs of dashes into a single dash
['/^\-|\-$/', ''] // Strip trailing/leading dash
];
}
// Convert all html entities to chars so encodings are uniform
$t = html_entity_decode($string);
// Do regex-based sanitization
foreach ($tsml_sanitize_data_sort_regexps as $a) {
$t = preg_replace($a[0], $a[1], $t);
}
// Unicode-aware lowercase of characters in string
if (function_exists('mb_strtolower')) {
return mb_strtolower($t);
} else {
return strtolower($t);
}
}
/**
* called by register_activation_hook in admin_import
* @return void
*/
function tsml_activate_data_source_scan()
{
// Use wp_next_scheduled to check if the event is already scheduled
$timestamp = wp_next_scheduled('tsml_scan_data_source');
// If $timestamp is false schedule scan since it hasn't been done previously
if ($timestamp == false) {
// Schedule the event for right now, then to reoccur daily using the hook 'tsml_scan_data_source'
wp_schedule_event(time(), 'daily', 'tsml_scan_data_source');
}
}
// function: scans passed data source url looking for recent updates
// used: fired by cron job tsml_scan_data_source
// todo - think about what the right place for this is
add_action('tsml_scan_data_source', function ($data_source_url) {
$data_source_name = null;
$data_source_parent_region_id = -1;
$data_source_count_meetings = 0;
$tsml_notification_addresses = tsml_get_option_array('tsml_notification_addresses');
$tsml_data_sources = tsml_get_option_array('tsml_data_sources');
$data_source_count_meetings = (int) $tsml_data_sources[$data_source_url]['count_meetings'];
if (empty($tsml_notification_addresses) || $data_source_count_meetings !== 0 || !array_key_exists($data_source_url, $tsml_data_sources)) {
return;
}
$data_source_name = $tsml_data_sources[$data_source_url]['name'];
$data_source_parent_region_id = $tsml_data_sources[$data_source_url]['parent_region_id'];
// try fetching
$response = wp_safe_remote_get($data_source_url, ['timeout' => 30, 'sslverify' => false]);
if (empty($response['body']) || !($body = json_decode($response['body'], true))) {
return;
}
// allow reformatting as necessary
$meetings = tsml_import_sanitize_meetings($body, $data_source_url, $data_source_parent_region_id);
// check import feed for changes and return array summing up changes detected
$change_log = tsml_import_get_changed_meetings($meetings, $data_source_url);
// send email notifying admins that this data source needs updating
if (is_array($change_log) && count($change_log)) {
// translators: %s is the name of the data source
$message = '' . sprintf(__('Please sign in to your website and refresh the %s feed on the Import & Export page.', '12-step-meeting-list'), $data_source_name) . '
';
$message .= tsml_import_build_change_report($change_log);
$import_page_url = admin_url('/edit.php?post_type=tsml_meeting&page=import');
$button_text = __('Go to Import & Export page', '12-step-meeting-list');
$message .= ' ' . $button_text . ' ';
$subject = __('Data Source Changes Detected', '12-step-meeting-list');
tsml_email($tsml_notification_addresses, $subject, $message);
}
}, 10, 1);
/**
* Creates and configures cron job to run a scheduled data source scan
*
* @param mixed $data_source_url
* @param mixed $data_source_name
* @return void
*/
function tsml_schedule_import_scan($data_source_url, $data_source_name)
{
$timestamp = tsml_strtotime('tomorrow midnight'); // Use tsml_strtotime to incorporate local site timezone with UTC.
// Get the timestamp for the next event when found.
$ts = wp_next_scheduled("tsml_scan_data_source", array($data_source_url));
if ($ts) {
$mydisplaytime = tsml_date_localised(get_option('date_format') . ' ' . get_option('time_format'), $ts); // Use tsml_date_localised to convert to specified format with local site timezone included.
tsml_alert("The $data_source_name data source's next scheduled run is $mydisplaytime. You can adjust the recurrences and the times that the job ('tsml_scan_data_source ') runs with the WP_Crontrol plugin.");
} else {
// When adding a data source we schedule its daily cron job
register_activation_hook(__FILE__, 'tsml_activate_data_source_scan');
// Schedule the refresh
if (wp_schedule_event($timestamp, "daily", "tsml_scan_data_source", array($data_source_url)) === false) {
tsml_alert("$data_source_name data source scan scheduling failed!");
} else {
$mydisplaytime = tsml_date_localised(get_option('date_format') . ' ' . get_option('time_format'), $timestamp); // Use tsml_date_localised to convert to specified format with local site timezone included.
tsml_alert("The $data_source_name data source's next scheduled run is $mydisplaytime. You can adjust the recurrences and the times that the job ('tsml_scan_data_source ') runs with the WP_Crontrol plugin.");
}
}
}
/**
* incorporates wp timezone into php's StrToTime() function
* used: here, admin-import.php
*
* @param mixed $str
* @return string
*/
function tsml_strtotime($str)
{
// This function behaves a bit like PHP's StrToTime() function, but taking into account the Wordpress site's timezone
// CAUTION: It will throw an exception when it receives invalid input - please catch it accordingly
// From https://mediarealm.com.au/
$tz_string = get_option('timezone_string');
$tz_offset = get_option('gmt_offset', 0);
if (!empty($tz_string)) {
// If site timezone option string exists, use it
$timezone = $tz_string;
} elseif ($tz_offset == 0) {
// get UTC offset, if it isn’t set then return UTC
$timezone = 'UTC';
} else {
$timezone = $tz_offset;
if (substr($tz_offset, 0, 1) != "-" && substr($tz_offset, 0, 1) != "+" && substr($tz_offset, 0, 1) != "U") {
$timezone = "+" . $tz_offset;
}
}
$datetime = new DateTime($str, new DateTimeZone($timezone));
return $datetime->format('U');
}
/**
* incorporates wp timezone into php's date() function
* used: here, admin-import.php
*
* @param mixed $format
* @param mixed $timestamp
* @return string
*/
function tsml_date_localised($format, $timestamp = null)
{
// This function behaves a bit like PHP's Date() function, but taking into account the Wordpress site's timezone
// CAUTION: It will throw an exception when it receives invalid input - please catch it accordingly
// From https://mediarealm.com.au/
$tz_string = get_option('timezone_string');
$tz_offset = get_option('gmt_offset', 0);
if (!empty($tz_string)) {
// If site timezone option string exists, use it
$timezone = $tz_string;
} elseif ($tz_offset == 0) {
// get UTC offset, if it isn’t set then return UTC
$timezone = 'UTC';
} else {
$timezone = $tz_offset;
if (substr($tz_offset, 0, 1) != "-" && substr($tz_offset, 0, 1) != "+" && substr($tz_offset, 0, 1) != "U") {
$timezone = "+" . $tz_offset;
}
}
if ($timestamp === null) {
$timestamp = time();
}
$datetime = new DateTime();
$datetime->setTimestamp($timestamp);
$datetime->setTimezone(new DateTimeZone($timezone));
return $datetime->format($format);
}
/**
* Compares content of an import meeting against local meeting
* @param array $meeting_old Old version of meeting (local version)
* @param array $meeting_new New version of meeting (imported version)
* @param boolean $compare_import [default false] Imported meetings use source fields for comparison
* @param boolean $translate_fields [default false] Translate system fields to field labels
* @return array|null
*/
function tsml_compare_meetings($meeting_old, $meeting_new, $compare_import = false, $translate_fields = false)
{
global $tsml_export_columns, $tsml_source_fields_map, $tsml_entity_fields, $tsml_array_fields;
$meeting_old = (array) $meeting_old;
$meeting_new = (array) $meeting_new;
// update local meeting with stored source field values
if ($compare_import) {
foreach ($tsml_source_fields_map as $source_field => $field) {
if (isset($meeting_old[$source_field])) {
$meeting_old[$field] = $meeting_old[$source_field];
} else {
unset($meeting_old[$field]);
}
}
}
$compare_fields = array_merge(array_keys($tsml_export_columns), $tsml_entity_fields);
$compare_fields = array_diff(
$compare_fields,
// these fields are internal fields, not content fields for comparison
explode(',', 'id,slug,author,data_source,data_source_name,updated,latitude,longitude')
);
// normalize meetings for comparison
$normalized_meetings = array();
foreach (array($meeting_old, $meeting_new) as $index => $meeting) {
$normalized_meetings[$index] = array();
foreach ($compare_fields as $field) {
$value = isset($meeting[$field]) ? $meeting[$field] : '';
if (in_array($field, $tsml_array_fields, true)) {
$value = empty($value) ? [] : ((array) $value);
}
// import meeting: post_title <=> name
if (!$value && 'name' === $field) {
$value = isset($meeting['post_title']) ? $meeting['post_title'] : '';
}
if (is_object($value) || is_array($value)) {
$value = json_encode($value);
} elseif (null !== $value) {
$value = trim(htmlspecialchars_decode(html_entity_decode(strval($value)), ENT_QUOTES));
} else {
$value = '';
}
$normalized_meetings[$index][$field] = $value;
}
}
// if 'group' is blank on both, we don't compare group_notes or district
if (!$normalized_meetings[0]['group'] && !$normalized_meetings[1]['group']) {
$compare_fields = array_diff($compare_fields, ['district', 'group_notes']);
}
$diff_fields = array();
foreach ($compare_fields as $field) {
if ($normalized_meetings[0][$field] !== $normalized_meetings[1][$field]) {
$diff_fields[] = ($translate_fields && isset($tsml_export_columns[$field])) ? $tsml_export_columns[$field] : $field;
}
}
return count($diff_fields) ? $diff_fields : null;
}
/**
* Return hash for imported meeting, for later comparison
*
* @param [array] $meeting
* @return string
*/
function tsml_get_import_hash($meeting)
{
$remove_fields = explode(',', ',row,import_hash');
if (is_array($meeting)) {
foreach ($meeting as $field => $value) {
if (in_array(strtolower($field), $remove_fields)) {
unset($meeting[$field]);
}
}
}
return md5(serialize($meeting));
}
function tsml_header()
{
if (function_exists('wp_is_block_theme') && wp_is_block_theme()) {
include(TSML_PATH . '/templates/header.php');
} else {
get_header();
}
}
function tsml_footer()
{
if (function_exists('wp_is_block_theme') && wp_is_block_theme()) {
include(TSML_PATH . '/templates/footer.php');
} else {
get_footer();
}
}
/**
* Redirect legacy query parameters to TSML UI's url structure
*/
function tsml_redirect_legacy_query_params()
{
global $tsml_program, $tsml_programs;
$replacements = [];
if (isset($_GET['tsml-attendance_option'])) {
if ($_GET['tsml-attendance_option'] === 'active') {
$replacements['type'][] = 'active';
} elseif ($_GET['tsml-attendance_option'] === 'in_person') {
$replacements['type'][] = 'in-person';
} elseif ($_GET['tsml-attendance_option'] === 'online') {
$replacements['type'][] = 'online';
}
}
if (isset($_GET['tsml-day'])) {
$days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
if (array_key_exists($_GET['tsml-day'], $days)) {
$replacements['weekday'] = $days[$_GET['tsml-day']];
}
}
if (isset($_GET['tsml-mode'])) {
$replacements['mode'] = $_GET['tsml-mode'];
if (isset($_GET['tsml-distance'])) {
$replacements['distance'] = $_GET['tsml-distance'];
} else {
$replacements['distance'] = 10;
}
}
if (isset($_GET['tsml-query'])) {
$replacements['search'] = $_GET['tsml-query'];
}
if (isset($_GET['tsml-region'])) {
$replacements['region'] = $_GET['tsml-region'];
}
if (isset($_GET['tsml-time'])) {
$replacements['time'] = $_GET['tsml-time'];
}
if (isset($_GET['tsml-type'])) {
$types = explode(',', $_GET['tsml-type']);
foreach ($types as $type) {
if (array_key_exists($type, $tsml_programs[$tsml_program]['types']))
$replacements['type'][] = sanitize_title($tsml_programs[$tsml_program]['types'][$type]);
}
}
if (isset($replacements['type'])) {
$replacements['type'] = implode('/', $replacements['type']);
}
if (count($replacements) > 0) {
wp_redirect(tsml_meetings_url($replacements));
}
}