html_entity_decode($location['location']),
'formatted_address' => $location['formatted_address'],
'latitude' => $location['latitude'],
'longitude' => $location['longitude'],
'region' => $location['region_id'],
'notes' => html_entity_decode($location['location_notes']),
'tokens' => tsml_string_tokens($location['location']),
'type' => 'location',
'url' => $location['location_url'],
);
}
wp_send_json($results);
}
}
//ajax for the search typeahead and the meeting edit group typeahead
add_action('wp_ajax_tsml_groups', 'tsml_ajax_groups');
add_action('wp_ajax_nopriv_tsml_groups', 'tsml_ajax_groups');
if (!function_exists('tsml_ajax_groups')) {
function tsml_ajax_groups() {
$groups = get_posts('post_type=tsml_group&numberposts=-1');
$results = array();
foreach ($groups as $group) {
$group_custom = get_post_meta($group->ID);
$results[] = array(
'value' => $group->post_title,
'website' => @$group_custom['website'][0],
'website_2' => @$group_custom['website_2'][0],
'email' => @$group_custom['email'][0],
'phone' => @$group_custom['phone'][0],
'contact_1_name' => @$group_custom['contact_1_name'][0],
'contact_1_email' => @$group_custom['contact_1_email'][0],
'contact_1_phone' => @$group_custom['contact_1_phone'][0],
'contact_2_name' => @$group_custom['contact_2_name'][0],
'contact_2_email' => @$group_custom['contact_2_email'][0],
'contact_2_phone' => @$group_custom['contact_2_phone'][0],
'contact_3_name' => @$group_custom['contact_3_name'][0],
'contact_3_email' => @$group_custom['contact_3_email'][0],
'contact_3_phone' => @$group_custom['contact_3_phone'][0],
'last_contact' => @$group_custom['last_contact'][0],
'notes' => $group->post_content,
'tokens' => tsml_string_tokens($title),
'type' => 'group',
);
}
wp_send_json($results);
}
}
//PDF meeting schedule linked on import & settings
add_action('wp_ajax_tsml_pdf', 'tsml_ajax_pdf');
add_action('wp_ajax_nopriv_tsml_pdf', 'tsml_ajax_pdf');
if (!function_exists('tsml_ajax_pdf')) {
function tsml_ajax_pdf() {
//include the file, which includes TCPDF
include(TSML_PATH . '/includes/pdf.php');
//create new PDF document
$pdf = new TSMLPDF(array(
'margin' => !empty($_GET['margin']) ? floatval($_GET['margin']) : .25,
'width' => !empty($_GET['width']) ? floatval($_GET['width']) : 4.25,
'height' => !empty($_GET['height']) ? floatval($_GET['height']) : 11,
));
//send to browser
if (!headers_sent()) {
$pdf->Output('meeting-schedule.pdf', 'I');
}
exit;
}
}
/*possible endpoint for future add/edit meeting webhook
add_action('wp_ajax_tsml_push', 'tsml_ajax_push');
add_action('wp_ajax_nopriv_tsml_push', 'tsml_ajax_push');
if (!function_exists('tsml_ajax_push')) {
function tsml_ajax_push() {
//todo check key for authorization
//update meeting
//send debug email to website admin
$message = '
' . print_r($_POST, true) . '
';
$headers = array('Content-Type: text/html; charset=UTF-8');
wp_mail(get_option('admin_email'), 'POST Data Received', $message, $headers);
die('POST received');
}
}
*/
//ajax for the search typeahead
add_action('wp_ajax_tsml_regions', 'tsml_ajax_regions');
add_action('wp_ajax_nopriv_tsml_regions', 'tsml_ajax_regions');
if (!function_exists('tsml_ajax_regions')) {
function tsml_ajax_regions() {
$regions = get_terms('tsml_region');
$results = array();
foreach ($regions as $region) {
$results[] = array(
'id' => $region->slug,
'value' => html_entity_decode($region->name),
'type' => 'region',
'tokens' => tsml_string_tokens($region->name),
);
}
wp_send_json($results);
}
}
//ajax for address checking
add_action('wp_ajax_tsml_address', 'tsml_admin_ajax_address');
if (!function_exists('tsml_admin_ajax_address')) {
function tsml_admin_ajax_address() {
if (!$posts = get_posts(array(
'post_type' => 'tsml_location',
'numberposts' => 1,
'meta_key' => 'formatted_address',
'meta_value' => sanitize_text_field($_GET['formatted_address']),
))) return array();
$region = array_values(get_the_terms($posts[0]->ID, 'tsml_region'));
//return info to user
wp_send_json(array(
'location' => $posts[0]->post_title,
'location_notes' => $posts[0]->post_content,
'region' => $region[0]->term_id,
));
}
}
//get all contact email addresses (for europe)
//linked from admin_import.php
add_action('wp_ajax_contacts', 'tsml_ajax_contacts');
if (!function_exists('tsml_ajax_contacts')) {
function tsml_ajax_contacts() {
global $wpdb;
$post_ids = $wpdb->get_col('SELECT id FROM ' . $wpdb->posts . ' WHERE post_type IN ("tsml_group", "tsml_meeting")');
$emails = $wpdb->get_col('SELECT meta_value FROM ' . $wpdb->postmeta . ' WHERE meta_key IN ("email", "contact_1_email", "contact_2_email", "contact_3_email") AND post_id IN (' . implode(',', $post_ids) . ')');
$emails = array_unique(array_filter($emails));
sort($emails);
die(implode(',
', $emails));
}
}
//function: export csv
//used: linked from admin-import.php, potentially also from theme
add_action('wp_ajax_csv', 'tsml_ajax_csv');
add_action('wp_ajax_nopriv_csv', 'tsml_ajax_csv');
if (!function_exists('tsml_ajax_csv')) {
function tsml_ajax_csv() {
//going to need this later
global $tsml_days, $tsml_programs, $tsml_program, $tsml_sharing;
//security
if (($tsml_sharing != 'open') && !is_user_logged_in()) {
tsml_ajax_unauthorized();
}
//get data source
$meetings = tsml_get_meetings(array(), true);
//define columns to output, always in English for portability (per Poland NA)
$columns = array(
'time' => 'Time',
'end_time' => 'End Time',
'day' => 'Day',
'name' => 'Name',
'location' => 'Location',
'formatted_address' => 'Address',
'region' => 'Region',
'sub_region' => 'Sub Region',
'types' => 'Types',
'notes' => 'Notes',
'location_notes' => 'Location Notes',
'group' => 'Group',
'district' => 'District',
'sub_district' => 'Sub District',
'website' => 'Website',
'website_2' => 'Website 2',
'venmo' => 'Venmo',
'email' => 'Email',
'phone' => 'Phone',
'group_notes' => 'Group Notes',
'contact_1_name' => 'Contact 1 Name',
'contact_1_email' => 'Contact 1 Email',
'contact_1_phone' => 'Contact 1 Phone',
'contact_2_name' => 'Contact 2 Name',
'contact_2_email' => 'Contact 2 Email',
'contact_2_phone' => 'Contact 2 Phone',
'contact_3_name' => 'Contact 3 Name',
'contact_3_email' => 'Contact 3 Email',
'contact_3_phone' => 'Contact 3 Phone',
'last_contact' => 'Last Contact',
'author' => 'Author',
'slug' => 'Slug',
'updated' => 'Updated',
);
//helper vars
$delimiter = ',';
$escape = '"';
//do header
$return = implode($delimiter, array_values($columns)) . PHP_EOL;
//get the preferred time format setting
$time_format = get_option('time_format');
//append meetings
foreach ($meetings as $meeting) {
$line = array();
foreach ($columns as $column=>$value) {
if (in_array($column, array('time', 'end_time'))) {
$line[] = empty($meeting[$column]) ? null : date($time_format, strtotime($meeting[$column]));
} elseif ($column == 'day') {
$line[] = $tsml_days[$meeting[$column]];
} elseif ($column == 'types') {
$types = $meeting[$column];
foreach ($types as &$type) $type = $tsml_programs[$tsml_program]['types'][trim($type)];
sort($types);
$line[] = $escape . implode(', ', $types) . $escape;
} elseif (strstr($column, 'notes')) {
$line[] = $escape . strip_tags(str_replace($escape, str_repeat($escape, 2), $meeting[$column])) . $escape;
} elseif (array_key_exists($column, $meeting)) {
$line[] = $escape . str_replace($escape, '', $meeting[$column]) . $escape;
} else {
$line[] = '';
}
}
$return .= implode($delimiter, $line) . PHP_EOL;
}
//headers to trigger file download
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="meetings.csv"');
//output
wp_die($return);
}
}
//function: receives user feedback, sends email to admin
//used: single-meetings.php
add_action('wp_ajax_tsml_feedback', 'tsml_ajax_feedback');
add_action('wp_ajax_nopriv_tsml_feedback', 'tsml_ajax_feedback');
if (!function_exists('tsml_ajax_feedback')) {
function tsml_ajax_feedback() {
global $tsml_feedback_addresses, $tsml_nonce;
$meeting = tsml_get_meeting(intval($_POST['meeting_id']));
$name = sanitize_text_field($_POST['tsml_name']);
$email = sanitize_email($_POST['tsml_email']);
$message = '' . nl2br(sanitize_text_area(stripslashes($_POST['tsml_message']))) . '
';
$message_lines = array(
__('Requested By', '12-step-meeting-list') => $name . ' <' . $email . '>',
__('Meeting', '12-step-meeting-list') => '' . $meeting->post_title . '',
__('When', '12-step-meeting-list') => tsml_format_day_and_time($meeting->day, $meeting->time),
);
if (!empty($meeting->types)) {
$message_lines[__('Types', '12-step-meeting-list')] = implode(', ', $meeting->types);
}
if (!empty($meeting->notes)) {
$message_lines[__('Notes', '12-step-meeting-list')] = $meeting->notes;
}
if (!empty($meeting->location)) {
$message_lines[__('Location', '12-step-meeting-list')] = $meeting->location;
}
if (!empty($meeting->formatted_address)) {
$message_lines[__('Address', '12-step-meeting-list')] = $meeting->formatted_address;
}
if (!empty($meeting->region)) {
$message_lines[__('Region', '12-step-meeting-list')] = $meeting->region;
}
if (!empty($meeting->location_notes)) {
$message_lines[__('Location Notes', '12-step-meeting-list')] = $meeting->location_notes;
}
foreach ($message_lines as $key => $value) {
$message .= '' . $key . ': ' . $value . '
';
}
//email vars
if (!isset($_POST['tsml_nonce']) || !wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) {
_e('Error: nonce value not set correctly. Email was not sent.', '12-step-meeting-list');
} elseif (empty($tsml_feedback_addresses) || empty($name) || !is_email($email) || empty($message)) {
_e('Error: required form value missing. Email was not sent.', '12-step-meeting-list');
} else {
//send HTML email
$subject = __('Meeting Feedback Form', '12-step-meeting-list') . ': ' . $meeting->post_title;
if (tsml_email($tsml_feedback_addresses, $subject, $message, $name . ' <' . $email . '>')) {
_e('Thank you for your feedback.', '12-step-meeting-list');
} else {
global $phpmailer;
if (!empty($phpmailer->ErrorInfo)) {
printf(__('Error: %s', '12-step-meeting-list'), $phpmailer->ErrorInfo);
} else {
_e('An error occurred while sending email!', '12-step-meeting-list');
}
}
remove_filter('wp_mail_content_type', 'tsml_email_content_type_html');
}
exit;
}
}
//function: receives user feedback, sends email to admin
//used: single-meetings.php
add_action('wp_ajax_tsml_geocode', 'tsml_ajax_geocode');
add_action('wp_ajax_nopriv_tsml_geocode', 'tsml_ajax_geocode');
if (!function_exists('tsml_ajax_geocode')) {
function tsml_ajax_geocode() {
global $tsml_nonce;
if (!wp_verify_nonce(@$_GET['nonce'], $tsml_nonce)) tsml_ajax_unauthorized();
wp_send_json(tsml_geocode(@$_GET['address']));
}
}
//ajax function to import the meetings in the import buffer
//used by admin_import.php
add_action('wp_ajax_tsml_import', 'tsml_ajax_import');
if (!function_exists('function_name')) {
function tsml_ajax_import() {
global $tsml_data_sources;
$meetings = get_option('tsml_import_buffer', array());
$errors = array();
$limit = 25;
//manage import buffer
if (count($meetings) > $limit) {
//slice off the first batch, save the remaining back to the import buffer
$remaining = array_slice($meetings, $limit);
update_option('tsml_import_buffer', $remaining);
$meetings = array_slice($meetings, 0, $limit);
} elseif (count($meetings)) {
//take them all and remove the option (don't wait, to prevent an endless loop)
$remaining = array();
delete_option('tsml_import_buffer');
}
//get lookups, todo consider adding regions to this
$locations = $groups = array();
$all_locations = tsml_get_locations();
foreach ($all_locations as $location) $locations[$location['formatted_address']] = $location['location_id'];
$all_groups = tsml_get_all_groups();
foreach ($all_groups as $group) $groups[$group->post_title] = $group->ID;
//passing post_modified and post_modified_gmt to wp_insert_post() below does not seem to work
//todo occasionally remove this to see if it is working
add_filter('wp_insert_post_data', 'tsml_import_post_modified', 99, 2);
foreach ($meetings as $meeting) {
//check address
if (empty($meeting['formatted_address'])) {
$errors[] = '' . sprintf(__('No location information provided for %s.', '12-step-meeting-list'), $meeting['name']) . '';
continue;
}
//geocode address
$geocoded = tsml_geocode($meeting['formatted_address']);
if ($geocoded['status'] == 'error') {
$errors[] = '' . $geocoded['reason'] . '';
continue;
}
//try to guess region from geocode
if (empty($meeting['region']) && !empty($geocoded['city'])) $meeting['region'] = $geocoded['city'];
//add region to taxonomy if it doesn't exist yet
if (!empty($meeting['region'])) {
if (!$term = term_exists($meeting['region'], 'tsml_region', 0)) {
$term = wp_insert_term($meeting['region'], 'tsml_region', 0);
}
$region_id = intval($term['term_id']);
//can only have a subregion if you already have a region
if (!empty($meeting['sub_region'])) {
if (!$term = term_exists($meeting['sub_region'], 'tsml_region', $region_id)) {
$term = wp_insert_term($meeting['sub_region'], 'tsml_region', array('parent'=>$region_id));
}
$region_id = intval($term['term_id']);
}
}
//handle group (can't have a group if group name not specified)
if (empty($meeting['group'])) {
$group_id = null;
} else {
if (!array_key_exists($meeting['group'], $groups)) {
$group_id = wp_insert_post(array(
'post_type' => 'tsml_group',
'post_status' => 'publish',
'post_title' => $meeting['group'],
'post_content' => empty($meeting['group_notes']) ? '' : $meeting['group_notes'],
));
//add district to taxonomy if it doesn't exist yet
if (!empty($meeting['district'])) {
if (!$term = term_exists($meeting['district'], 'tsml_district', 0)) {
$term = wp_insert_term($meeting['district'], 'tsml_district', 0);
}
$district_id = intval($term['term_id']);
//can only have a subregion if you already have a region
if (!empty($meeting['sub_district'])) {
if (!$term = term_exists($meeting['sub_district'], 'tsml_district', $district_id)) {
$term = wp_insert_term($meeting['sub_district'], 'tsml_district', array('parent'=>$district_id));
}
$district_id = intval($term['term_id']);
}
wp_set_object_terms($group_id, $district_id, 'tsml_district');
}
$groups[$meeting['group']] = $group_id;
} else {
$group_id = $groups[$meeting['group']];
}
}
//save location if not already in the database
if (array_key_exists($geocoded['formatted_address'], $locations)) {
$location_id = $locations[$geocoded['formatted_address']];
} else {
$location_id = wp_insert_post(array(
'post_title' => $meeting['location'],
'post_type' => 'tsml_location',
'post_content' => $meeting['location_notes'],
'post_status' => 'publish',
));
$locations[$geocoded['formatted_address']] = $location_id;
add_post_meta($location_id, 'formatted_address', $geocoded['formatted_address']);
add_post_meta($location_id, 'latitude', $geocoded['latitude']);
add_post_meta($location_id, 'longitude', $geocoded['longitude']);
wp_set_object_terms($location_id, $region_id, 'tsml_region');
}
//save meeting to this location
$options = array(
'post_title' => $meeting['name'],
'post_type' => 'tsml_meeting',
'post_status' => 'publish',
'post_parent' => $location_id,
'post_content' => trim($meeting['notes']), //not sure why recursive trim not catching this
'post_modified' => $meeting['post_modified'],
'post_modified_gmt' => $meeting['post_modified_gmt'],
'post_author' => $meeting['post_author'],
);
if (!empty($meeting['slug'])) $options['post_name'] = $meeting['slug'];
$meeting_id = wp_insert_post($options);
//add day and time(s) if not appointment meeting
if (!empty($meeting['time']) && (!empty($meeting['day']) || (string) $meeting['day'] === '0')) {
add_post_meta($meeting_id, 'day', $meeting['day']);
add_post_meta($meeting_id, 'time', $meeting['time']);
if (!empty($meeting['end_time'])) add_post_meta($meeting_id, 'end_time', $meeting['end_time']);
}
//add types, group, and data_source if available
if (!empty($meeting['types'])) add_post_meta($meeting_id, 'types', $meeting['types']);
if (!empty($meeting['group'])) add_post_meta($meeting_id, 'group_id', $groups[$meeting['group']]);
if (!empty($meeting['data_source'])) add_post_meta($meeting_id, 'data_source', $meeting['data_source']);
//handle contact information (could be meeting or group)
$contact_entity_id = empty($group_id) ? $meeting_id : $group_id;
for ($i = 1; $i <= GROUP_CONTACT_COUNT; $i++) {
foreach (array('name', 'phone', 'email') as $field) {
$key = 'contact_' . $i . '_' . $field;
if (!empty($meeting[$key])) update_post_meta($contact_entity_id, $key, $meeting[$key]);
}
}
if (!empty($meeting['website'])) {
update_post_meta($contact_entity_id, 'website', esc_url_raw($meeting['website'], array('http', 'https')));
}
if (!empty($meeting['website_2'])) {
update_post_meta($contact_entity_id, 'website_2', esc_url_raw($meeting['website_2'], array('http', 'https')));
}
if (!empty($meeting['email'])) {
update_post_meta($contact_entity_id, 'email', $meeting['email']);
}
if (!empty($meeting['phone'])) {
update_post_meta($contact_entity_id, 'phone', $meeting['phone']);
}
if (!empty($meeting['last_contact']) && ($last_contact = strtotime($meeting['last_contact']))) {
update_post_meta($contact_entity_id, 'last_contact', date('Y-m-d', $last_contact));
}
}
//have to update the cache of types in use
tsml_cache_rebuild();
//have to update the cache of types in use
tsml_update_types_in_use();
//update viewport biasing for geocoding
tsml_bounds();
//remove post_modified thing added earlier
remove_filter('wp_insert_post_data', 'tsml_import_post_modified', 99);
//send json result to browser
$meetings = tsml_count_meetings();
$locations = tsml_count_locations();
$regions = tsml_count_regions();
$groups = tsml_count_groups();
//update the data source counts for the database
foreach ($tsml_data_sources as $url => $props) {
$tsml_data_sources[$url]['count_meetings'] = count(tsml_get_data_source_ids($url));
}
update_option('tsml_data_sources', $tsml_data_sources);
//now format the counts for JSON output
foreach ($tsml_data_sources as $url => $props) {
$tsml_data_sources[$url]['count_meetings'] = number_format($props['count_meetings']);
}
wp_send_json(array(
'errors' => $errors,
'remaining' => count($remaining),
'counts' => compact('meetings', 'locations', 'regions', 'groups'),
'data_sources' => $tsml_data_sources,
'descriptions' => array(
'meetings' => sprintf(_n('%s meeting', '%s meetings', $meetings, '12-step-meeting-list'), number_format_i18n($meetings)),
'locations' => sprintf(_n('%s location', '%s locations', $locations, '12-step-meeting-list'), number_format_i18n($locations)),
'groups' => sprintf(_n('%s group', '%s groups', $groups, '12-step-meeting-list'), number_format_i18n($groups)),
'regions' => sprintf(_n('%s region', '%s regions', $regions, '12-step-meeting-list'), number_format_i18n($regions)),
),
));
}
}
//api ajax function
//used by theme, web app, mobile app
add_action('wp_ajax_meetings', 'tsml_ajax_meetings');
add_action('wp_ajax_nopriv_meetings', 'tsml_ajax_meetings');
if (!function_exists('tsml_ajax_meetings')) {
function tsml_ajax_meetings() {
global $tsml_sharing, $tsml_sharing_keys, $tsml_nonce;
//accepts GET or POST
$input = empty($_POST) ? $_GET : $_POST;
if ($tsml_sharing == 'open') {
//sharing is open
} elseif (!empty($input['nonce']) && wp_verify_nonce($input['nonce'], $tsml_nonce)) {
//nonce checks out
} elseif (!empty($input['key']) && array_key_exists($input['key'], $tsml_sharing_keys)) {
//key checks out
} else {
tsml_ajax_unauthorized();
}
if (!headers_sent()) header('Access-Control-Allow-Origin: *');
wp_send_json(tsml_get_meetings($input));
}
}
//create and email a sharing key to meeting guide
add_action('wp_ajax_meeting_guide', 'tsml_ajax_meeting_guide');
add_action('wp_ajax_nopriv_meeting_guide', 'tsml_ajax_meeting_guide');
if (!function_exists('tsml_ajax_meeting_guide')) {
function tsml_ajax_meeting_guide() {
global $tsml_sharing, $tsml_sharing_keys;
$mg_key = false;
//check for existing keys
foreach ($tsml_sharing_keys as $key => $value) {
if ($value == 'Meeting Guide') {
$mg_key = $key;
}
}
//add new key
if (empty($mg_key)) {
$mg_key = md5(uniqid('Meeting Guide', true));
$tsml_sharing_keys[$mg_key] = 'Meeting Guide';
asort($tsml_sharing_keys);
update_option('tsml_sharing_keys', $tsml_sharing_keys);
}
//build url
$message = admin_url('admin-ajax.php?') . http_build_query(array(
'action' => 'meetings',
'key' => $mg_key,
));
//send email
if (tsml_email(TSML_CONTACT_EMAIL, 'Sharing Key', $message)) {
die('sent');
}
die('not sent!');
}
}
//send a 401 and exit
function tsml_ajax_unauthorized() {
if (!headers_sent()) header('HTTP/1.1 401 Unauthorized', true, 401);
wp_send_json(array('error' => 'HTTP/1.1 401 Unauthorized'));
}