0) { if ($_FILES['tsml_import']['error'] == 1) { $error = __('The uploaded file exceeds the upload_max_filesize directive in php.ini.', '12-step-meeting-list'); } elseif ($_FILES['tsml_import']['error'] == 2) { $error = __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', '12-step-meeting-list'); } elseif ($_FILES['tsml_import']['error'] == 3) { $error = __('The uploaded file was only partially uploaded.', '12-step-meeting-list'); } elseif ($_FILES['tsml_import']['error'] == 4) { $error = __('No file was uploaded.', '12-step-meeting-list'); } elseif ($_FILES['tsml_import']['error'] == 6) { $error = __('Missing a temporary folder.', '12-step-meeting-list'); } elseif ($_FILES['tsml_import']['error'] == 7) { $error = __('Failed to write file to disk.', '12-step-meeting-list'); } elseif ($_FILES['tsml_import']['error'] == 8) { $error = __('A PHP extension stopped the file upload.', '12-step-meeting-list'); } else { $error = sprintf(__('File upload error #%d', '12-step-meeting-list'), $_FILES['tsml_import']['error']); } } elseif (empty($extension)) { $error = __('Uploaded file did not have a file extension. Please add .csv to the end of the file name.', '12-step-meeting-list'); } elseif (!in_array($extension, array('csv', 'txt'))) { $error = sprintf(__('Please upload a csv file. Your file ended in .%s.', '12-step-meeting-list'), $extension); } elseif (!$handle = fopen($_FILES['tsml_import']['tmp_name'], 'r')) { $error = __('Error opening CSV file', '12-step-meeting-list'); } else { //extract meetings from CSV while (($data = fgetcsv($handle, 3000, ',')) !== false) { //skip empty rows if (strlen(trim(implode($data)))) { $meetings[] = $data; } } //remove any rows that aren't arrays $meetings = array_filter($meetings, 'is_array'); //crash if no data if (count($meetings) < 2) { $error = __('Nothing was imported because no data rows were found.', '12-step-meeting-list'); } else { //allow theme-defined function to reformat CSV prior to import (New Hampshire, Ventura) if (function_exists('tsml_import_reformat')) { $meetings = tsml_import_reformat($meetings); } //if it's FNV data, reformat it $meetings = tsml_import_reformat_fnv($meetings); //get header $header = array_shift($meetings); $header = array_map('sanitize_title_with_dashes', $header); $header = str_replace('-', '_', $header); $header_count = count($header); //check header for required fields if (!in_array('address', $header) && !in_array('city', $header)) { $error = __('Either Address or City is required.', '12-step-meeting-list'); } else { //loop through data and convert to array foreach ($meetings as &$meeting) { //check length if ($header_count > count($meeting)) { $meeting = array_pad($meeting, $header_count, null); } elseif ($header_count < count($meeting)) { $meeting = array_slice($meeting, 0, $header_count); } //associate $meeting = array_combine($header, $meeting); } //import into buffer, also done this way in data source import tsml_import_buffer_set($meetings); //run deletes if ($_POST['delete'] == 'regions') { //get all regions present in array $regions = array(); foreach ($meetings as $meeting) { $regions[] = empty($meeting['sub_region']) ? $meeting['region'] : $meeting['sub_region']; } //get locations for those meetings $location_ids = get_posts(array( 'post_type' => 'tsml_location', 'numberposts' => -1, 'fields' => 'ids', 'tax_query' => array( array( 'taxonomy' => 'tsml_region', 'field' => 'name', 'terms' => array_unique($regions), ), ), )); //get posts for those meetings $meeting_ids = get_posts(array( 'post_type' => 'tsml_meeting', 'numberposts' => -1, 'fields' => 'ids', 'post_parent__in' => $location_ids, )); tsml_delete($meeting_ids); tsml_delete_orphans(); } elseif ($_POST['delete'] == 'no_data_source') { tsml_delete(get_posts(array( 'post_type' => 'tsml_meeting', 'numberposts' => -1, 'fields' => 'ids', 'meta_query' => array( array( 'key' => 'data_source', 'compare' => 'NOT EXISTS', 'value' => '', ), ), ))); tsml_delete_orphans(); } elseif ($_POST['delete'] == 'all') { tsml_delete('everything'); } } } } } //add data source if (!empty($_POST['tsml_add_data_source']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { //sanitize URL $_POST['tsml_add_data_source'] = trim(esc_url_raw($_POST['tsml_add_data_source'], array('http', 'https'))); //try fetching $response = wp_remote_get($_POST['tsml_add_data_source'], array( 'timeout' => 30, 'sslverify' => false, )); if (is_array($response) && !empty($response['body']) && ($body = json_decode($response['body'], true))) { //if already set, hard refresh if (array_key_exists($_POST['tsml_add_data_source'], $tsml_data_sources)) { tsml_delete(tsml_get_data_source_ids($_POST['tsml_add_data_source'])); tsml_delete_orphans(); } $tsml_data_sources[$_POST['tsml_add_data_source']] = array( 'status' => 'OK', 'last_import' => current_time('timestamp'), 'count_meetings' => 0, 'name' => sanitize_text_field($_POST['tsml_add_data_source_name']), 'type' => 'JSON', ); //import feed tsml_import_buffer_set($body, $_POST['tsml_add_data_source']); //save data source configuration update_option('tsml_data_sources', $tsml_data_sources); } elseif (!is_array($response)) { tsml_alert(__('Invalid response,
' . print_r($response, true) . '
.', '12-step-meeting-list'), 'error'); } elseif (empty($response['body'])) { tsml_alert(__('Data source gave an empty response, you might need to try again.', '12-step-meeting-list'), 'error'); } else { switch (json_last_error()) { case JSON_ERROR_NONE: tsml_alert(__('JSON: no errors.', '12-step-meeting-list'), 'error'); break; case JSON_ERROR_DEPTH: tsml_alert(__('JSON: Maximum stack depth exceeded.', '12-step-meeting-list'), 'error'); break; case JSON_ERROR_STATE_MISMATCH: tsml_alert(__('JSON: Underflow or the modes mismatch.', '12-step-meeting-list'), 'error'); break; case JSON_ERROR_CTRL_CHAR: tsml_alert(__('JSON: Unexpected control character found.', '12-step-meeting-list'), 'error'); break; case JSON_ERROR_SYNTAX: tsml_alert(__('JSON: Syntax error, malformed JSON.', '12-step-meeting-list'), 'error'); break; case JSON_ERROR_UTF8: tsml_alert(__('JSON: Malformed UTF-8 characters, possibly incorrectly encoded.', '12-step-meeting-list'), 'error'); break; default: tsml_alert(__('JSON: Unknown error.', '12-step-meeting-list'), 'error'); break; } } } //check for existing import buffer $meetings = get_option('tsml_import_buffer', array()); //remove data source if (!empty($_POST['tsml_remove_data_source'])) { //sanitize URL $_POST['tsml_remove_data_source'] = esc_url_raw($_POST['tsml_remove_data_source'], array('http', 'https')); if (array_key_exists($_POST['tsml_remove_data_source'], $tsml_data_sources)) { //remove all meetings for this data source tsml_delete(tsml_get_data_source_ids($_POST['tsml_remove_data_source'])); //clean up orphaned locations & groups tsml_delete_orphans(); //remove data source unset($tsml_data_sources[$_POST['tsml_remove_data_source']]); update_option('tsml_data_sources', $tsml_data_sources); tsml_alert(__('Data source removed.', '12-step-meeting-list')); } } //change program if (!empty($_POST['tsml_program']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $tsml_program = sanitize_text_field($_POST['tsml_program']); update_option('tsml_program', $tsml_program); tsml_alert(__('Program setting updated.', '12-step-meeting-list')); } //change distance units if (!empty($_POST['tsml_distance_units']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $tsml_distance_units = ($_POST['tsml_distance_units'] == 'mi') ? 'mi' : 'km'; update_option('tsml_distance_units', $tsml_distance_units); tsml_alert(__('Distance units updated.', '12-step-meeting-list')); } //change distance units if (!empty($_POST['tsml_contact_display']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $tsml_contact_display = ($_POST['tsml_contact_display'] == 'public') ? 'public' : 'private'; update_option('tsml_contact_display', $tsml_contact_display); tsml_cache_rebuild(); //this value affects what's in the cache tsml_alert(__('Contact privacy updated.', '12-step-meeting-list')); } //change sharing setting if (!empty($_POST['tsml_sharing']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $tsml_sharing = ($_POST['tsml_sharing'] == 'open') ? 'open' : 'restricted'; update_option('tsml_sharing', $tsml_sharing); tsml_alert(__('Sharing setting updated.', '12-step-meeting-list')); } //add a sharing key if (!empty($_POST['tsml_add_sharing_key']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $name = sanitize_text_field($_POST['tsml_add_sharing_key']); $key = md5(uniqid($name, true)); $tsml_sharing_keys[$key] = $name; asort($tsml_sharing_keys); update_option('tsml_sharing_keys', $tsml_sharing_keys); tsml_alert(__('Sharing key added.', '12-step-meeting-list')); //users might expect that if they add "meeting guide" that then they are added to the app if (strtolower($name) == 'meeting guide') { $current_user = wp_get_current_user(); $message = admin_url('admin-ajax.php?') . http_build_query(array( 'action' => 'meetings', 'key' => $key, )); tsml_email(TSML_CONTACT_EMAIL, 'Sharing Key', $message, $current_user->user_email); } } //remove a sharing key if (!empty($_POST['tsml_remove_sharing_key']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $key = sanitize_text_field($_POST['tsml_remove_sharing_key']); if (array_key_exists($key, $tsml_sharing_keys)) { unset($tsml_sharing_keys[$key]); if (empty($tsml_sharing_keys)) { delete_option('tsml_sharing_keys'); } else { update_option('tsml_sharing_keys', $tsml_sharing_keys); } tsml_alert(__('Sharing key removed.', '12-step-meeting-list')); } else { //theoretically should never get here, because user is choosing from a list tsml_alert(sprintf(esc_html__('

%s was not found in the list of sharing keys. Please try again.

', '12-step-meeting-list'), $key), 'error'); } } //add a feedback email if (!empty($_POST['tsml_add_feedback_address']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $email = sanitize_text_field($_POST['tsml_add_feedback_address']); if (!is_email($email)) { //theoretically should never get here, because WordPress checks entry first tsml_alert(sprintf(esc_html__('%s is not a valid email address. Please try again.', '12-step-meeting-list'), $email), 'error'); } else { $tsml_feedback_addresses[] = $email; $tsml_feedback_addresses = array_unique($tsml_feedback_addresses); sort($tsml_feedback_addresses); update_option('tsml_feedback_addresses', $tsml_feedback_addresses); tsml_alert(__('Feedback address added.', '12-step-meeting-list')); } } //remove a feedback email if (!empty($_POST['tsml_remove_feedback_address']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $email = sanitize_text_field($_POST['tsml_remove_feedback_address']); if (($key = array_search($email, $tsml_feedback_addresses)) !== false) { unset($tsml_feedback_addresses[$key]); if (empty($tsml_feedback_addresses)) { delete_option('tsml_feedback_addresses'); } else { update_option('tsml_feedback_addresses', $tsml_feedback_addresses); } tsml_alert(__('Feedback address removed.', '12-step-meeting-list')); } else { //theoretically should never get here, because user is choosing from a list tsml_alert(sprintf(esc_html__('

%s was not found in the list of addresses. Please try again.

', '12-step-meeting-list'), $email), 'error'); } } //add a notification email if (!empty($_POST['tsml_add_notification_address']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $email = sanitize_text_field($_POST['tsml_add_notification_address']); if (!is_email($email)) { //theoretically should never get here, because WordPress checks entry first tsml_alert(sprintf(esc_html__('

%s is not a valid email address. Please try again.

', '12-step-meeting-list'), $email), 'error'); } else { $tsml_notification_addresses[] = $email; $tsml_notification_addresses = array_unique($tsml_notification_addresses); sort($tsml_notification_addresses); update_option('tsml_notification_addresses', $tsml_notification_addresses); tsml_alert(__('Notification address added.', '12-step-meeting-list')); } } //remove a notification email if (!empty($_POST['tsml_remove_notification_address']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $email = sanitize_text_field($_POST['tsml_remove_notification_address']); if (($key = array_search($email, $tsml_notification_addresses)) !== false) { unset($tsml_notification_addresses[$key]); if (empty($tsml_notification_addresses)) { delete_option('tsml_notification_addresses'); } else { update_option('tsml_notification_addresses', $tsml_notification_addresses); } tsml_alert(__('Notification address removed.', '12-step-meeting-list')); } else { //theoretically should never get here, because user is choosing from a list tsml_alert(sprintf(esc_html__('

%s was not found in the list of addresses. Please try again.

', '12-step-meeting-list'), $email), 'error'); } } //add a Mapbox access token if (isset($_POST['tsml_add_mapbox_key']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $tsml_mapbox_key = sanitize_text_field($_POST['tsml_add_mapbox_key']); if (empty($tsml_mapbox_key)) { delete_option('tsml_mapbox_key'); tsml_alert(__('API key removed.', '12-step-meeting-list')); } else { update_option('tsml_mapbox_key', $tsml_mapbox_key); tsml_alert(__('API key saved.', '12-step-meeting-list')); } //there can be only one $tsml_google_maps_key = null; delete_option('tsml_google_maps_key'); } //add an API key if (isset($_POST['tsml_add_google_maps_key']) && isset($_POST['tsml_nonce']) && wp_verify_nonce($_POST['tsml_nonce'], $tsml_nonce)) { $key = sanitize_text_field($_POST['tsml_add_google_maps_key']); if (empty($key)) { delete_option('tsml_google_maps_key'); tsml_alert(__('API key removed.', '12-step-meeting-list')); } else { update_option('tsml_google_maps_key', $key); $tsml_google_maps_key = $key; tsml_alert(__('API key saved.', '12-step-meeting-list')); } //there can be only one $tsml_mapbox_key = null; delete_option('tsml_mapbox_key'); } /*debugging delete_option('tsml_data_sources'); tsml_delete('everything'); tsml_delete_orphans(); */ ?>

Enable Maps on Your Site

Formerly, this plugin came with Google Maps built in, but, due to a change in Google's pricing, that's no longer possible. Read more about the price increase here.

Now if you want to enable maps on your site you have two options: Mapbox or Google. They are both good options! In all likelihood neither one will charge you money. Mapbox gives 50,000 free map views / month, Google gives 28,500 free views. That's a lot of traffic!

To sign up for Mapbox go here. You will only need a valid email address, no credit card required. Copy your access token and paste it below:

Alternatively you may still use Google. Their interface is slightly more complex, because they offer more services. Go here to get a key from Google. The process should only take a few minutes, although you will have to enter a credit card. Here are some instructions.

Be sure to:
Enable the Google Maps Javascript API
Secure your credentials by adding your website URL to the list of allowed referrers

Once you're done, paste your new key below.


__('don\'t delete anything', '12-step-meeting-list'), 'regions' => __('delete only the meetings, locations, and groups for the regions present in this CSV', '12-step-meeting-list'), 'all' => __('delete all meetings, locations, groups, districts, and regions', '12-step-meeting-list'), ); if (!empty($tsml_data_sources)) { $delete_options['no_data_source'] = __('delete all meetings, locations, and groups not from a data source', '12-step-meeting-list'); } $delete_selected = (empty($_POST['delete']) || !array_key_exists($_POST['delete'], $delete_options)) ? 'nothing' : $_POST['delete']; foreach ($delete_options as $key => $value) {?>

  • Time, if present, should be in a standard date format such as 6:00 AM or 06:00. Non-standard or empty dates will be imported as "by appointment."', '12-step-meeting-list')?>
  • End Time, if present, should be in a standard date format such as 6:00 AM or 06:00.', '12-step-meeting-list')?>
  • Day if present, should either Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, or Saturday. Meetings that occur on multiple days should be listed separately. \'Daily\' or \'Mondays\' will not work. Non-standard days will be imported as "by appointment."', '12-step-meeting-list')?>
  • Name is the name of the meeting, and is optional, although it\'s valuable information for the user. If it\'s missing, a name will be created by combining the location, day, and time.', '12-step-meeting-list')?>
  • Slug optional, and sets the meeting post\'s "slug" or unique string, which is used in the URL. This should be unique to the meeting. Setting this helps preserve user bookmarks.', '12-step-meeting-list')?>
  • Location is the name of the location, and is optional. Generally it\'s the group or building name. If it\'s missing, the address will be used. In the event that there are multiple location names for the same address, the first location name will be used.', '12-step-meeting-list')?>
  • Address is strongly encouraged and will be corrected by Google, so it may look different afterward. Ideally, every address for the same location should be exactly identical, and not contain extra information about the address, such as the building name or descriptors like "around back."', '12-step-meeting-list')?>
  • Address is specified, then City, State, and Country are optional, but they might be useful if your addresses sound ambiguous to Google. If address is not specified, then these fields are required.', '12-step-meeting-list')?>
  • Notes are freeform notes that are specific to the meeting. For example, "last Saturday is birthday night."', '12-step-meeting-list')?>
  • Region is user-defined and can be anything. Often this is a small municipality or neighborhood. Since these go in a dropdown, ideally you would have 10 to 20 regions, although it\'s ok to be over or under.', '12-step-meeting-list')?>
  • Sub Region makes the Region hierarchical; in San Jose we have sub regions for East San Jose, West San Jose, etc. New York City might have Manhattan be a Region, and Greenwich Village be a Sub Region.', '12-step-meeting-list')?>
  • Location Notes are freeform notes that will show up on every meeting that this location. For example, "Enter from the side."', '12-step-meeting-list')?>
  • Group is a way of grouping contacts. Meetings with the same group name will be linked and share contact information.', '12-step-meeting-list')?>
  • District is user-defined and can be anything, but should be a string rather than an integer, e.g. \'District 01\' rather than \'1.\' A group name must also be specified.', '12-step-meeting-list')?>
  • Sub District makes the District hierachical.', '12-step-meeting-list')?>
  • Group Notes is for stuff like a short group history, or when the business meeting meets.', '12-step-meeting-list')?>
  • Website and Website 2 are optional.', '12-step-meeting-list')?>
  • Email is optional. This is a public email address.', '12-step-meeting-list')?>
  • Phone is optional. This is a public phone number.', '12-step-meeting-list')?>
  • Contact 1/2/3 Name/Email/Phone (nine fields in total) are all optional. By default, contact information is only visible inside the WordPress dashboard.', '12-step-meeting-list')?>
  • Last Contact is an optional date.', '12-step-meeting-list')?>
  • Types should be a comma-separated list of the following options. This list is determined by which program is selected at right.', '12-step-meeting-list')?>

ServiceNumber.', '12-step-meeting-list')?>

right here. More information is available at the Meeting Guide API Specification.', '12-step-meeting-list'), admin_url('admin-ajax.php') . '?action=meetings', 'https://github.com/meeting-guide/spec')?>

$properties) {?>

%s, while WordPress recommends PHP %s or above. This can cause unexpected errors. Please contact your host and upgrade!', '12-step-meeting-list'), PHP_VERSION, 'https://wordpress.org/about/requirements/', '5.6')?>

right here. Link that page from your site\'s nav menu to make it visible to the public.', '12-step-meeting-list'), get_post_type_archive_link('tsml_meeting'))?>

CSV format.', '12-step-meeting-list'), admin_url('admin-ajax.php') . '?action=csv')?>

half page and full page.', '12-step-meeting-list'), admin_url('admin-ajax.php') . '?action=tsml_pdf', admin_url('admin-ajax.php') . '?action=tsml_pdf&width=8.5')?>

class="hidden">

Click here to see their email addresses.', '12-step-meeting-list'), admin_url('admin-ajax.php') . '?action=contacts')?>

let us know what types of meetings it has (Open, Closed, Topic Discussion, etc).', '12-step-meeting-list'), TSML_CONTACT_EMAIL, rawurlencode('WordPress New Program Request'))?>

meetings feed.', '12-step-meeting-list'), admin_url('admin-ajax.php?action=meetings'))?>

Meeting Guide App.')?>

$name) { $address = admin_url('admin-ajax.php?') . http_build_query(array( 'action' => 'meetings', 'key' => $key, )); ?>