= 200 && $status_code <= 299) { return false; } $body = strtolower((string) wp_remote_retrieve_body($response)); $content_type = strtolower((string) wp_remote_retrieve_header($response, 'content-type')); $cf_mitigated = strtolower((string) wp_remote_retrieve_header($response, 'cf-mitigated')); // explicit Cloudflare challenge signal if ($cf_mitigated === 'challenge') { return true; } // challenges are usually HTML interstitials, not feed payloads $is_html = (strpos($content_type, 'text/html') !== false) || (strpos($body, 'get_error_message()); } if (!is_array($response)) { // translators: %s is the invalid response from the data source return sprintf(__('Invalid response:
%s
.', '12-step-meeting-list'), print_r($response, true)); } $response_code = (int) wp_remote_retrieve_response_code($response); if ($response_code < 200 || $response_code > 299) { // security services answer automated requests with an HTML challenge instead of the feed if (tsml_import_is_security_challenge_response($response)) { // translators: %s is the HTTP status code return sprintf(__('The data source returned HTTP %s because a security service challenged the request. Whoever hosts the feed will need to allow requests from this server.', '12-step-meeting-list'), $response_code); } // translators: %1$s is the HTTP status code, %2$s is the HTTP status message return sprintf(__('The data source returned HTTP %1$s %2$s.', '12-step-meeting-list'), $response_code, wp_remote_retrieve_response_message($response)); } if (empty($response['body'])) { return __('Data source gave an empty response, you might need to try again.', '12-step-meeting-list'); } return null; } /** * trigger import of a data source to the import buffer * * @param mixed $data_source_url * @param mixed $data_source_name * @param mixed $data_source_parent_region_id * @param mixed $data_source_change_detect * @return void */ function tsml_import_data_source($data_source_url, $data_source_name = '', $data_source_parent_region_id = 0, $data_source_change_detect = 'disabled') { global $tsml_debug; $tsml_data_sources = tsml_get_option_array('tsml_data_sources'); // sanitize URL, name, parent region id, and Change Detection values $data_source_url = $imported_data_source_url = trim(esc_url_raw($data_source_url, ['http', 'https'])); $data_source_name = sanitize_text_field($data_source_name); $data_source_parent_region_id = (int) $data_source_parent_region_id; $data_source_change_detect = sanitize_text_field($data_source_change_detect); // data source $current_data_source = array_key_exists($data_source_url, $tsml_data_sources) ? $tsml_data_sources[$data_source_url] : null; if ($current_data_source) { $data_source_name = $current_data_source['name']; $data_source_parent_region_id = $current_data_source['parent_region_id']; $data_source_change_detect = $current_data_source['change_detect']; } // use c4r sheets service for Google Sheets URLs if (strpos($data_source_url, 'docs.google.com/spreadsheets') !== false) { $url_parts = explode('/', $data_source_url); $sheet_id = $url_parts[5]; $imported_data_source_url = 'https://sheets.code4recovery.org/tsml/' . $sheet_id; } // initialize variables $import_meetings = $delete_meeting_ids = []; // try fetching $response = wp_safe_remote_get($imported_data_source_url, [ 'timeout' => 30, 'sslverify' => false, ]); $response_error = tsml_import_response_error($response); if (!$response_error && ($meetings = json_decode($response['body'], true))) { // allow reformatting as necessary $meetings = tsml_import_sanitize_meetings($meetings, $imported_data_source_url, $data_source_parent_region_id); // actual meetings to import $import_meetings = []; // for new data sources we import every meeting, otherwise test for changed meetings if (!$current_data_source) { $import_meetings = $meetings; $current_data_source = [ 'status' => 'OK', 'last_import' => current_time('timestamp'), 'count_meetings' => 0, 'name' => $data_source_name, 'parent_region_id' => $data_source_parent_region_id, 'change_detect' => $data_source_change_detect, 'type' => 'JSON', ]; // create a cron job to run daily when Change Detection is enabled for the new data source if ($data_source_change_detect === 'enabled') { tsml_schedule_import_scan($data_source_url, $data_source_name); } tsml_log( 'data_source', __('Added', '12-step-meeting-list') . ' - ' . $data_source_name, // translators: %s is the number of meetings sprintf(__('%s meetings', '12-step-meeting-list'), count($import_meetings)) ); } else { // get updated feed import record set $change_log = tsml_import_get_changed_meetings($meetings, $data_source_url); tsml_log( 'data_source', __('Updates queued', '12-step-meeting-list') . ' - ' . $data_source_name, // translators: %s is the number of updates sprintf(__('%s updates', '12-step-meeting-list'), count($change_log)) ); // drop meetings that weren't found in the import foreach ($change_log as $change) { $meeting = (array) $change['meeting']; // just avoiding bad returns if (empty($meeting)) { continue; } $info = $change['info']; if ('remove' === $change['action']) { // log meeting delete tsml_log( 'import_meeting', $info, $data_source_name . ' - ' . $meeting['name'] . ' - ' . tsml_format_day_and_time($meeting['day'], $meeting['time'], ' @ ', true) ); $delete_meeting_ids[] = $change['meeting_id']; } else { $import_meetings[] = $meeting; } } if (count($delete_meeting_ids)) { tsml_delete($delete_meeting_ids); } tsml_delete_orphans(); if (count($change_log) && $tsml_debug) { $message = tsml_import_build_change_report($change_log); tsml_alert($message, 'info'); } // empty change log means we're up to date if (!count($change_log)) { tsml_alert(__('Your meeting list is already in sync with the feed.', '12-step-meeting-list'), 'success'); } // update data source timestamp $current_data_source['last_import'] = current_time('timestamp'); } // import feed tsml_import_buffer_set($import_meetings, $data_source_url, $data_source_parent_region_id); // save data source configuration $tsml_data_sources[$data_source_url] = $current_data_source; update_option('tsml_data_sources', $tsml_data_sources); } else { // a usable response that failed to parse is the only case left to describe $error_msg = $response_error; if (!$error_msg) { switch (json_last_error()) { case JSON_ERROR_NONE: $error_msg = __('JSON: no errors.', '12-step-meeting-list'); break; case JSON_ERROR_DEPTH: $error_msg = __('JSON: Maximum stack depth exceeded.', '12-step-meeting-list'); break; case JSON_ERROR_STATE_MISMATCH: $error_msg = __('JSON: Underflow or the modes mismatch.', '12-step-meeting-list'); break; case JSON_ERROR_CTRL_CHAR: $error_msg = __('JSON: Unexpected control character found.', '12-step-meeting-list'); break; case JSON_ERROR_SYNTAX: $error_msg = __('JSON: Syntax error, malformed JSON.', '12-step-meeting-list'); break; case JSON_ERROR_UTF8: $error_msg = __('JSON: Malformed UTF-8 characters, possibly incorrectly encoded.', '12-step-meeting-list'); break; default: $error_msg = __('JSON: Unknown error.', '12-step-meeting-list'); break; } } tsml_log('data_source_error', __('Import failed', '12-step-meeting-list') . ' - ' . $data_source_name, $error_msg); tsml_alert($error_msg, 'error'); } } /** * pull a set of meetings from the import buffer to import * * @param int $limit How many meetings to import, default 25, max 50 * @return array an array for the upstream AJAX call */ function tsml_import_buffer_next($limit = 25) { global $tsml_custom_meeting_fields, $tsml_source_fields_map, $tsml_contact_fields, $tsml_import_fields, $tsml_entity_fields, $tsml_array_fields; $meetings = tsml_get_option_array('tsml_import_buffer'); $tsml_data_sources = tsml_get_option_array('tsml_data_sources'); $errors = $remaining = []; $limit = max(1, min(50, intval($limit))); // 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) delete_option('tsml_import_buffer'); } // get lookups, todo consider adding regions to this $locations = $groups = []; $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); $data_source_parent_region_id = 0; foreach ($meetings as $meeting) { // if meeting id is passed, this is an update vs new meeting $meeting_id = intval(isset($meeting['ID']) ? $meeting['ID'] : 0); $data_source_url = array_key_exists('data_source', $meeting) ? $meeting['data_source'] : ''; $data_source = array_key_exists($data_source_url, $tsml_data_sources) ? $tsml_data_sources[$data_source_url] : null; $data_source_name = $data_source ? $data_source['name'] : ''; $is_new_meeting = !$meeting_id; // check address if (empty($meeting['formatted_address'])) { $errors[] = '
  • ' . // translators: %s is the meeting name sprintf(__('No location information provided for %s.', '12-step-meeting-list'), $meeting['name']) . '
  • '; continue; } // store incoming $tsml_source_fields_map fields for future comparisons: formatted_address, region, sub_region, slug foreach ($tsml_source_fields_map as $source_field => $field) { $meeting[$source_field] = isset($meeting[$field]) ? $meeting[$field] : ''; } // geocode address $geocoded = tsml_geocode($meeting['formatted_address']); if (array_key_exists('status', $geocoded) && $geocoded['status'] == 'error') { $errors[] = '
  • ' . $geocoded['reason'] . '
  • '; continue; } if ($data_source_parent_region_id == 0 && $data_source) { $data_source_parent_region_id = intval($data_source['parent_region_id']) != -1 ? intval($data_source['parent_region_id']) : 0; } // 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'])) { $args = ['parent' => $data_source_parent_region_id,]; if (!$term = term_exists($meeting['region'], 'tsml_region', $args['parent'])) { $term = wp_insert_term($meeting['region'], 'tsml_region', $args); } $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', ['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 { // has group if (!array_key_exists($meeting['group'], $groups)) { $group_id = wp_insert_post([ 'post_type' => 'tsml_group', 'post_status' => 'publish', 'post_title' => $meeting['group'], 'post_content' => empty($meeting['group_notes']) ? '' : $meeting['group_notes'], ]); $groups[$meeting['group']] = $group_id; } else { $group_id = $groups[$meeting['group']]; wp_update_post([ 'ID' => $group_id, '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 subdistrict if you already have a district 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', ['parent' => $district_id]); } $district_id = intval($term['term_id']); } wp_set_object_terms($group_id, $district_id, 'tsml_district'); } elseif (!$is_new_meeting) { wp_set_object_terms($group_id, null, 'tsml_district'); } } // save location if not already in the database if (array_key_exists($geocoded['formatted_address'], $locations)) { $location_id = $locations[$geocoded['formatted_address']]; wp_update_post([ 'ID' => $location_id, 'post_title' => $meeting['location'], 'post_content' => $meeting['location_notes'], ]); } else { $location_id = wp_insert_post([ 'post_title' => $meeting['location'], 'post_type' => 'tsml_location', 'post_content' => $meeting['location_notes'], 'post_status' => 'publish', ]); $locations[$geocoded['formatted_address']] = $location_id; } if ($location_id) { update_post_meta($location_id, 'formatted_address', $geocoded['formatted_address']); update_post_meta($location_id, 'latitude', $geocoded['latitude']); update_post_meta($location_id, 'longitude', $geocoded['longitude']); update_post_meta($location_id, 'approximate', $geocoded['approximate']); wp_set_object_terms($location_id, $region_id, 'tsml_region'); // timezone if (!empty($meeting['timezone'])) { update_post_meta($location_id, 'timezone', $meeting['timezone']); } else { delete_post_meta($location_id, 'timezone'); } } // save meeting to this location $options = [ '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 ID is included, this is a meeting update if ($meeting_id) { $options['ID'] = $meeting_id; } if (!empty($meeting['slug'])) { $options['post_name'] = $meeting['slug']; } // @TODO: check if $meeting_id is WP_Error / failed DB change $meeting_id = wp_insert_post($options); // log meeting $info = ''; if (array_key_exists('last_import_note', $meeting)) { $info = $meeting['last_import_note']; } else { $info = $is_new_meeting ? __('Meeting added', '12-step-meeting-list') : __('Meeting updated', '12-step-meeting-list'); } tsml_log('import_meeting', $info, array( 'input' => $data_source_name . ' - ' . $meeting['name'] . ' - ' . tsml_format_day_and_time($meeting['day'], $meeting['time'], ' @ ', true), 'meeting_id' => $meeting_id, )); // add day and time(s) if not appointment meeting if (!empty($meeting['time']) && (!empty($meeting['day']) || (string) $meeting['day'] === '0')) { update_post_meta($meeting_id, 'day', $meeting['day']); update_post_meta($meeting_id, 'time', $meeting['time']); update_post_meta($meeting_id, 'end_time', isset($meeting['end_time']) ? $meeting['end_time'] : ''); } // add custom meeting fields if available $custom_meeting_fields = array_merge( ['types', 'data_source', 'conference_url', 'conference_url_notes', 'conference_phone', 'conference_phone_notes'], array_keys($tsml_source_fields_map), $tsml_import_fields, $tsml_entity_fields ); if (!empty($tsml_custom_meeting_fields)) { $custom_meeting_fields = array_merge($custom_meeting_fields, array_keys($tsml_custom_meeting_fields)); } foreach ($custom_meeting_fields as $key) { if (empty($meeting[$key])) { if (metadata_exists('post', $meeting_id, $key)) { delete_post_meta($meeting_id, $key); } } else { update_post_meta($meeting_id, $key, $meeting[$key]); } } // Add Group Id and group specific info if applicable if (!empty($group_id)) { // link group to meeting update_post_meta($meeting_id, 'group_id', $group_id); } // handle contact information (could be meeting or group) $contact_entity_id = empty($group_id) ? $meeting_id : $group_id; $contact_meta = array(); for ($i = 1; $i <= TSML_GROUP_CONTACT_COUNT; $i++) { foreach (['name', 'phone', 'email'] as $field) { $key = 'contact_' . $i . '_' . $field; $contact_meta[$key] = isset($meeting[$key]) ? $meeting[$key] : ''; } } // update all contact fields (incoming blanks overwrite local blanks) foreach ($tsml_contact_fields as $contact_field => $field_type) { $value = isset($meeting[$contact_field]) ? $meeting[$contact_field] : ''; if ('url' === $field_type) { $value = esc_url_raw($value, ['http', 'https']); } if ('date' === $field_type) { $date = strtotime($value); $value = ($date) ? date('Y-m-d', $date) : ''; } $contact_meta[$contact_field] = $value; } // apply contact updates foreach ($contact_meta as $key => $value) { if (empty($value)) { if (metadata_exists('post', $contact_entity_id, $key)) { delete_post_meta($contact_entity_id, $key); } } else { update_post_meta($contact_entity_id, $key, $value); } } } // clean up orphaned locations & groups tsml_delete_orphans(); // get latest counts $meetings = tsml_count_meetings(); $locations = tsml_count_locations(); $regions = tsml_count_regions(); $groups = tsml_count_groups(); // update the data source counts in 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); // update ths TSML cache 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); // 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']); } return [ 'errors' => $errors, 'remaining' => count($remaining), 'counts' => compact('meetings', 'locations', 'regions', 'groups'), 'data_sources' => $tsml_data_sources, 'descriptions' => [ // translators: %s is the number of meetings 'meetings' => sprintf(_n('%s meeting', '%s meetings', $meetings, '12-step-meeting-list'), number_format_i18n($meetings)), // translators: %s is the number of locations 'locations' => sprintf(_n('%s location', '%s locations', $locations, '12-step-meeting-list'), number_format_i18n($locations)), // translators: %s is the number of groups 'groups' => sprintf(_n('%s group', '%s groups', $groups, '12-step-meeting-list'), number_format_i18n($groups)), // translators: %s is the number of regions 'regions' => sprintf(_n('%s region', '%s regions', $regions, '12-step-meeting-list'), number_format_i18n($regions)), ], ]; } /** * sanitize and import an array of meetings to an 'import buffer' (an wp_option that's iterated on progressively) * called from admin_import.php (both CSV and JSON) * * @param mixed $meetings * @param mixed $data_source_url * @param mixed $data_source_parent_region_id * @return void */ function tsml_import_buffer_set($meetings, $data_source_url = null, $data_source_parent_region_id = null) { $tsml_data_sources = tsml_get_option_array('tsml_data_sources'); /* * Most of the meeting transformation code has been extracted to a function so it can be executed early before the the Data Comparison code. * Only those feed records deemed to be out-of-sync with those already stored locally will be passed here. Google Sheets and CSV Imports do * not pass through the compare code and are passed here directly where the full meetings feed is transformed before updating. * */ foreach ($meetings as $i => $meeting) { // If the meeting doesn't have a data_source, use the one from the function call $meeting_data_source = isset($meeting['data_source']) ? $meeting['data_source'] : ''; if (empty($meeting_data_source)) { $meetings[$i]['data_source'] = $data_source_url; } else { // Check if this data sources is in our list of feeds if (!array_key_exists($meeting_data_source, $tsml_data_sources)) { // Not already there, so add it $tsml_data_sources[$meeting_data_source] = [ 'status' => 'OK', 'last_import' => current_time('timestamp'), 'count_meetings' => 0, 'name' => parse_url($meeting_data_source, PHP_URL_HOST), 'parent_region_id' => $data_source_parent_region_id, 'change_detect' => null, 'type' => 'JSON', ]; } } $meetings[$i]['data_source_parent_region_id'] = $data_source_parent_region_id; } // save data source configuration update_option('tsml_data_sources', $tsml_data_sources); // prepare import buffer in wp_options update_option('tsml_import_buffer', $meetings, false); } /** * Build output table to report on import changes * @param array $change_log Built from tsml_import_get_changed_meetings() * @param boolean $return_array Optionally return report as array * @return string|array */ function tsml_import_build_change_report($change_log, $return_array = false) { global $tsml_days; $rows = []; $message = ''; foreach ($change_log as $change_entry) { $meeting_id = isset($change_entry['meeting_id']) ? $change_entry['meeting_id'] : ''; $meeting = (array) $change_entry['meeting']; $meeting_name = isset($meeting['name']) ? $meeting['name'] : $meeting['slug']; $meeting_day_val = intval(isset($meeting['day']) ? $meeting['day'] : -1); $meeting_day = isset($tsml_days[$meeting_day_val]) ? $tsml_days[$meeting_day_val] : '?'; $meeting_time = isset($meeting['time']) ? tsml_format_time($meeting['time']) : '?'; $info = isset($change_entry['info']) ? $change_entry['info'] : ''; $permalink = get_permalink($meeting_id); if ($permalink !== false) { $meeting_name_linked = '' . $meeting_name . '
    '; } else { $meeting_name_linked = $meeting_name; } if ($return_array) { $rows[] = [ 'date' => time(), 'meeting_id' => $meeting_id, 'meeting' => "$meeting_name - $meeting_day @ $meeting_time", 'info' => $info, ]; } else { $message .= '' . ''; } } $message .= '
    ' . $meeting_name_linked . ' ' . $meeting_day . ' @ ' . $meeting_time . ' ' . $info . '
    '; if ($return_array) { return $rows; } return $message; } /** * Checks a list of imported meetings for necessary import changes / updates * * @param array $feed_meetings array of feed meetings to test for changes * @param string $data_source_url feed source url * @param int $data_source_parent_region_id feed parent region id * @return array array of changes, including action (add, remove, update), meeting, and meeting_id if available */ function tsml_import_get_changed_meetings($feed_meetings, $data_source_url) { $delete_meeting_ids = $source_meetings = $found_meeting_ids = []; $change_log = []; // get local meetings $data_source_ids = tsml_get_data_source_ids($data_source_url); sort($data_source_ids); // filter out all but the data source meetings $source_meetings = tsml_get_meetings(['data_source' => $data_source_url], false, true); // create array of database slugs for matching $meeting_slugs_map = array_column($source_meetings, 'slug', 'id'); $meeting_ids = array_keys($meeting_slugs_map); // create another array of the database source_slugs for matching to the original slug which may be overriden by the importer $meeting_source_slugs_map = array_column($source_meetings, 'source_slug', 'id'); // list changed and new meetings found in the data source feed foreach ($feed_meetings as $index => $feed_meeting) { $feed_meeting_slug = $source_meeting = $source_meeting_id = null; $feed_meeting['import_hash'] = tsml_get_import_hash($feed_meeting); if (empty($feed_meeting) || !is_array($feed_meeting)) { continue; } $feed_meeting_slug = isset($feed_meeting['slug']) ? $feed_meeting['slug'] : null; $source_meeting_id = array_search($feed_meeting_slug, $meeting_source_slugs_map); if (!$source_meeting_id) { $source_meeting_id = array_search($feed_meeting_slug, $meeting_slugs_map); } if ($source_meeting_id) { $source_meeting_index = array_search($source_meeting_id, $meeting_ids); $source_meeting = ($source_meeting_index !== false) ? $source_meetings[$source_meeting_index] : null; } // if we found a local meeting, compare for update need if ($source_meeting) { $source_meeting = (array) $source_meeting; $found_meeting_ids[] = $source_meeting['id']; if (!empty($source_meeting['import_hash']) && $source_meeting['import_hash'] === $feed_meeting['import_hash']) { continue; } $changed_fields = tsml_compare_meetings($source_meeting, $feed_meeting, true, true); if (!empty($changed_fields)) { $feed_meeting['last_import'] = current_time('timestamp'); $feed_meeting['last_import_note'] = __('Meeting updated', '12-step-meeting-list') . ': ' . implode(', ', $changed_fields); // add `ID` field to meeting to trigger an existing post update versus new post insert $feed_meeting['ID'] = $source_meeting_id; $change_log[] = array( 'meeting' => $feed_meeting, 'meeting_id' => $source_meeting_id, 'action' => 'update', 'info' => $feed_meeting['last_import_note'], ); } } else { $feed_meeting['last_import'] = current_time('timestamp'); $feed_meeting['last_import_note'] = __('Meeting added', '12-step-meeting-list'); $change_log[] = array( 'meeting' => $feed_meeting, 'action' => 'add', 'info' => $feed_meeting['last_import_note'], ); } } // meetings to delete will be any we didn't find in feed $delete_meeting_ids = array_diff($meeting_ids, $found_meeting_ids); // add to change log foreach ($delete_meeting_ids as $delete_meeting_id) { $source_meeting_index = array_search($delete_meeting_id, $meeting_ids); $delete_meeting = ($source_meeting_index !== false) ? $source_meetings[$source_meeting_index] : null; $change_log[] = array( 'meeting' => $delete_meeting, 'meeting_id' => $delete_meeting_id, 'action' => 'remove', 'info' => __('Meeting removed', '12-step-meeting-list'), ); } return $change_log; } /** * filter workaround for setting post_modified dates * used in tsml_ajax_import * * @param mixed $data * @param mixed $postarr * @return mixed */ function tsml_import_post_modified($data, $postarr) { if (!empty($postarr['post_modified'])) { $data['post_modified'] = $postarr['post_modified']; } if (!empty($postarr['post_modified_gmt'])) { $data['post_modified_gmt'] = $postarr['post_modified_gmt']; } return $data; } /** * function: translates a Meeting Guide format Google Sheet to proper format for import * used: tsml_import_buffer_set * * @param mixed $data * @return array */ function tsml_import_reformat_googlesheet($data) { $meetings = []; $header = array_shift($data['values']); $header = array_map('sanitize_title_with_dashes', $header); $header = str_replace('-', '_', $header); $header_count = count($header); foreach ($data['values'] as $row) { // creates a meeting array with elements corresponding to each column header of the Google Sheet; updated for Google Sheets v4 API $meeting = []; for ($j = 0; $j < $header_count; $j++) { if (isset($row[$j])) { $meeting[$header[$j]] = $row[$j]; } } array_push($meetings, $meeting); } return $meetings; } /** * return array of sanitized meetings * sanitizes imported meetings before processing * * @param mixed $meetings * @param mixed $data_source_url * @param mixed $data_source_parent_region_id * @return array|object */ function tsml_import_sanitize_meetings($meetings, $data_source_url = null, $data_source_parent_region_id = null) { global $tsml_programs, $tsml_program, $tsml_days, $tsml_meeting_attendance_options, $tsml_contact_fields, $tsml_entity_fields, $tsml_array_fields; //track group fields and unique_group_values $group_fields = array_keys($tsml_contact_fields); for ($i = 1; $i <= TSML_GROUP_CONTACT_COUNT; $i++) { foreach (['name', 'phone', 'email'] as $field) { $group_fields[] = 'contact_' . $i . '_' . $field; } } $group_fields[] = 'group_notes'; $groups = []; // track locations to normalize $locations = []; $location_fields = [ 'latitude', 'longitude', 'approximate', 'location', 'location_notes', 'timezone', 'region', ]; // track sanitized meeting slug counts $meeting_slugs = array(); // handle requests to Google Sheets API (obsolete - remove on or after May 2025) if (strpos($data_source_url, "sheets.googleapis.com") !== false) { tsml_alert(__('You can now add a Google Sheet directly to TSML. Please replace this feed with the Sheet URL. We will be dropping support for the Google Sheets API in a future release.', '12-step-meeting-list'), 'warning'); $meetings = tsml_import_reformat_googlesheet($meetings); } // handle requests to C4R sheets service if (strpos($data_source_url, "sheets.code4recovery.org/tsml") !== false) { if (count($meetings['warnings'])) { $warnings = __('The following issues were detected with this Google Sheet:', '12-step-meeting-list') . '

      ' . implode(array_map(function ($warning) { return '
    1. ' . $warning['error'] . ': ' . implode(array_map(function ($value) { return '' . $value . ''; }, $warning['value'])) . '
    2. '; }, $meetings['warnings'])) . '