= 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[] = '
%s.', '12-step-meeting-list'), $meeting['name']) .
'| ' . $meeting_name_linked . ' ' . $meeting_day . ' @ ' . $meeting_time . ' | ' . '' . $info . ' |
' . $value . '';
}, $warning['value'])) .
'';
tsml_alert($warnings, 'warning');
}
$meetings = $meetings['meetings'];
}
// allow theme-defined function to reformat data source import - issue #439
if (function_exists('tsml_import_reformat')) {
// phpcs:ignore
$meetings = tsml_import_reformat($meetings);
}
// uppercasing for value matching later
$upper_types = array_map('strtoupper', $tsml_programs[$tsml_program]['types']);
$upper_days = array_map('strtoupper', $tsml_days);
// fix legacy/alternate type names
$type_aliases = [
'aa' => [
'SIGN LANGUAGE' => 'ASL',
'OUTDOOR MEETING' => 'OUT',
'STEP MEETING' => 'ST',
]
];
// get users, keyed by username
$users = [];
foreach (get_users(['fields' => ['ID', 'user_login'],]) as $user) {
$users[$user->user_login] = $user->ID;
}
$user_id = get_current_user_id();
// convert the array to UTF-8
if (function_exists('mb_detect_encoding')) {
array_walk_recursive($meetings, function ($value) {
if (!mb_detect_encoding((string) $value, 'utf-8', true)) {
return (string) mb_convert_encoding($value, 'UTF-8', 'auto');
}
return $value;
});
}
// trim and sanitize everything
array_walk_recursive($meetings, function (&$value, $key) {
// preserve
s as line breaks if present, otherwise clean up
$value = preg_replace('/\
/i', PHP_EOL, (string) $value);
$value = stripslashes($value);
$value = trim($value);
// turn "string" into string (only do if on both ends though)
if ((substr($value, 0, 1) == '"') && (substr($value, -1) == '"')) {
$value = trim(trim($value, '"'));
}
// sanitize
if (tsml_string_ends($key, 'notes')) {
$value = tsml_sanitize_text_area($value);
} else {
$value = sanitize_text_field($value);
}
return $value;
});
// check for any meetings with arrays of days and creates an individual meeting for each day in array
$meetings_to_add = [];
$indexes_to_remove = [];
for ($i = 0; $i < count($meetings); $i++) {
// figure out a singular slug from available sources, starting with 'slug' (of course)
$meeting_slug = '';
foreach (array('slug', 'post_name', 'ID', 'id', 'name', 'title', 'group', 'location') as $field) {
if (isset($meetings[$i][$field])) {
$meeting_slug = sanitize_title($meetings[$i][$field]);
}
if ($meeting_slug) {
break;
}
}
// if we have no slug at this point, something went wrong - remove the meeting and continue
if (!$meeting_slug) {
array_splice($meetings, $i, 1);
--$i;
continue;
}
// ensure every slug is unique for this import list
if (isset($meeting_slugs[$meeting_slug])) {
while (isset($meeting_slugs[$meeting_slug . '-' . $meeting_slugs[$meeting_slug]]) && $meeting_slugs[$meeting_slug] < 999) {
$meeting_slugs[$meeting_slug]++;
}
$meeting_slug = $meeting_slug . '-' . $meeting_slugs[$meeting_slug]++;
} else {
$meeting_slugs[$meeting_slug] = 1;
}
$meetings[$i]['slug'] = $meeting_slug;
if (isset($meetings[$i]['day']) && is_array($meetings[$i]['day'])) {
array_push($indexes_to_remove, $i);
foreach ($meetings[$i]['day'] as $single_day) {
$temp_meeting = $meetings[$i];
$temp_meeting['day'] = $single_day;
$temp_meeting['slug'] = $meetings[$i]['slug'] . "-" . $single_day;
array_push($meetings_to_add, $temp_meeting);
}
}
}
for ($i = 0; $i < count($indexes_to_remove); $i++) {
unset($meetings[$indexes_to_remove[$i]]);
}
$meetings = array_merge($meetings, $meetings_to_add);
// prepare array for import buffer
$count_meetings = count($meetings);
for ($i = 0; $i < $count_meetings; $i++) {
// column aliases
if (empty($meetings[$i]['postal_code']) && !empty($meetings[$i]['zip'])) {
$meetings[$i]['postal_code'] = $meetings[$i]['zip'];
}
if (empty($meetings[$i]['name']) && !empty($meetings[$i]['meeting'])) {
$meetings[$i]['name'] = $meetings[$i]['meeting'];
}
if (empty($meetings[$i]['location']) && !empty($meetings[$i]['location_name'])) {
$meetings[$i]['location'] = $meetings[$i]['location_name'];
}
if (empty($meetings[$i]['time']) && !empty($meetings[$i]['start_time'])) {
$meetings[$i]['time'] = $meetings[$i]['start_time'];
}
// if '@' is in address, remove it and everything after
if (!empty($meetings[$i]['address']) && $pos = strpos($meetings[$i]['address'], '@')) {
$meetings[$i]['address'] = trim(substr($meetings[$i]['address'], 0, $pos));
}
// if location name is missing, use address
if (empty($meetings[$i]['location'])) {
$meetings[$i]['location'] = empty($meetings[$i]['address']) ? __('Meeting Location', '12-step-meeting-list') : $meetings[$i]['address'];
}
// day can either be 0, 1, 2, 3 or Sunday, Monday, or empty
if (isset($meetings[$i]['day']) && !array_key_exists($meetings[$i]['day'], $upper_days)) {
$meetings[$i]['day'] = array_search(strtoupper($meetings[$i]['day']), $upper_days);
}
// sanitize time & day
if (empty($meetings[$i]['time']) || ($meetings[$i]['day'] === false)) {
$meetings[$i]['time'] = $meetings[$i]['end_time'] = $meetings[$i]['day'] = false; //by appointment
// if meeting name missing, use location
if (empty($meetings[$i]['name'])) {
// translators: %s is the location name
$meetings[$i]['name'] = sprintf(__('%s by Appointment', '12-step-meeting-list'), $meetings[$i]['location']);
}
} else {
// if meeting name missing, use location, day, and time
if (empty($meetings[$i]['name'])) {
// translators: %1s is the location name, %2s is the day, %3s is the time
$meetings[$i]['name'] = sprintf(__('%1$1s %2$2s at %3$3s', '12-step-meeting-list'), $meetings[$i]['location'], $tsml_days[$meetings[$i]['day']], $meetings[$i]['time']);
}
$meetings[$i]['time'] = tsml_format_time_reverse($meetings[$i]['time']);
if (!empty($meetings[$i]['end_time'])) {
$meetings[$i]['end_time'] = tsml_format_time_reverse($meetings[$i]['end_time']);
}
}
// google prefers USA for geocoding
if (!empty($meetings[$i]['country']) && $meetings[$i]['country'] == 'US') {
$meetings[$i]['country'] = 'USA';
}
// build address
if (empty($meetings[$i]['formatted_address'])) {
$address = [];
if (!empty($meetings[$i]['address'])) {
$address[] = $meetings[$i]['address'];
}
if (!empty($meetings[$i]['city'])) {
$address[] = $meetings[$i]['city'];
}
if (!empty($meetings[$i]['state'])) {
$address[] = $meetings[$i]['state'];
}
if (!empty($meetings[$i]['postal_code'])) {
if ((strlen($meetings[$i]['postal_code']) < 5) && ($meetings[$i]['country'] == 'USA')) {
$meetings[$i]['postal_code'] = str_pad($meetings[$i]['postal_code'], 5, '0', STR_PAD_LEFT);
}
$address[] = $meetings[$i]['postal_code'];
}
if (!empty($meetings[$i]['country'])) {
$address[] = $meetings[$i]['country'];
}
$meetings[$i]['formatted_address'] = implode(', ', $address);
}
// notes
if (empty($meetings[$i]['notes'])) {
$meetings[$i]['notes'] = '';
}
if (empty($meetings[$i]['location_notes'])) {
$meetings[$i]['location_notes'] = '';
}
if (empty($meetings[$i]['group_notes'])) {
$meetings[$i]['group_notes'] = '';
}
// updated
if (empty($meetings[$i]['updated']) || (!$meetings[$i]['updated'] = strtotime($meetings[$i]['updated']))) {
$meetings[$i]['updated'] = time();
}
$meetings[$i]['post_modified'] = date('Y-m-d H:i:s', $meetings[$i]['updated']);
$meetings[$i]['post_modified_gmt'] = get_gmt_from_date($meetings[$i]['post_modified']);
// author
if (!empty($meetings[$i]['author']) && array_key_exists($meetings[$i]['author'], $users)) {
$meetings[$i]['post_author'] = $users[$meetings[$i]['author']];
} else {
$meetings[$i]['post_author'] = $user_id;
}
// default region to city if not specified
if (empty($meetings[$i]['region']) && !empty($meetings[$i]['city'])) {
$meetings[$i]['region'] = $meetings[$i]['city'];
}
// sanitize types (they can be Closed or C)
if (empty($meetings[$i]['types'])) {
$meetings[$i]['types'] = [];
}
$types = $meetings[$i]['types'];
if (is_string($types)) {
$types = explode(',', $types);
}
$meetings[$i]['types'] = $unused_types = [];
foreach ($types as $type) {
$type = trim($type);
$upper_type = strtoupper($type);
if (!$type) {
continue;
}
if (in_array($upper_type, array_map('strtoupper', array_keys($upper_types)))) {
$meetings[$i]['types'][] = $type;
} elseif (in_array($upper_type, array_values($upper_types))) {
$meetings[$i]['types'][] = array_search($upper_type, $upper_types);
} elseif (array_key_exists($tsml_program, $type_aliases) && array_key_exists($upper_type, $type_aliases[$tsml_program])) {
$meetings[$i]['types'][] = $type_aliases[$tsml_program][$upper_type];
} else {
$unused_types[] = $type;
}
}
// if a meeting is both open and closed, make it closed
if (in_array('C', $meetings[$i]['types']) && in_array('O', $meetings[$i]['types'])) {
$meetings[$i]['types'] = array_diff($meetings[$i]['types'], ['O']);
}
// If Conference URL, validate; or if phone, force 'ONL' type, else remove 'ONL'
$meetings[$i]['types'] = array_values(array_diff($meetings[$i]['types'], ['ONL']));
if (!empty($meetings[$i]['conference_url'])) {
$url = esc_url_raw($meetings[$i]['conference_url'], ['http', 'https']);
if (tsml_conference_provider($url)) {
$meetings[$i]['conference_url'] = $url;
$meetings[$i]['types'][] = 'ONL';
} else {
$meetings[$i]['conference_url'] = null;
$meetings[$i]['conference_url_notes'] = null;
}
}
if (!empty($meetings[$i]['conference_phone']) && empty($meetings[$i]['conference_url'])) {
$meetings[$i]['types'][] = 'ONL';
}
if (empty($meetings[$i]['conference_phone'])) {
$meetings[$i]['conference_phone_notes'] = null;
}
// Clean up attendance options
if (!empty($meetings[$i]['attendance_option'])) {
$meetings[$i]['attendance_option'] = trim(strtolower($meetings[$i]['attendance_option']));
if (!array_key_exists($meetings[$i]['attendance_option'], $tsml_meeting_attendance_options)) {
$meetings[$i]['attendance_option'] = '';
}
}
// make sure we're not double-listing types
$meetings[$i]['types'] = array_unique($meetings[$i]['types']);
// clean up
foreach (['address', 'city', 'state', 'postal_code', 'country', 'updated'] as $key) {
if (isset($meetings[$i][$key])) {
unset($meetings[$i][$key]);
}
}
// sanitize timezone if present
if (!empty($meetings[$i]['timezone'])) {
// make an attempt to find timezone
$timezone = tsml_timezone_parse($meetings[$i]['timezone']);
if ($timezone) {
$meetings[$i]['timezone'] = $timezone;
} else {
unset($meetings[$i]['timezone']);
}
}
// collect group values
if (!empty($meetings[$i]['group'])) {
$group = $meetings[$i]['group'];
if (!isset($groups[$group])) {
$groups[$group] = array();
}
// currently first group value wins
foreach ($group_fields as $group_field) {
if (!empty($meetings[$i][$group_field]) && !isset($groups[$group][$group_field])) {
$groups[$group][$group_field] = $meetings[$i][$group_field];
}
}
}
// collect location values
if (!empty($meetings[$i]['formatted_address'])) {
$formatted_address = $meetings[$i]['formatted_address'];
if (!isset($locations[$formatted_address])) {
$locations[$formatted_address] = array();
}
foreach ($location_fields as $location_field) {
if (!empty($meetings[$i][$location_field]) && !isset($locations[$formatted_address][$location_field])) {
$locations[$formatted_address][$location_field] = $meetings[$i][$location_field];
}
}
}
// cleanup entity fields
foreach ($tsml_entity_fields as $field) {
if (isset($meetings[$i][$field])) {
// feedback_emails is an array
if (in_array($field, $tsml_array_fields, true)) {
$meetings[$i][$field] = array_values((array) $meetings[$i][$field]);
} else {
$meetings[$i][$field] = substr(trim(strval($meetings[$i][$field])), 0, 100);
}
}
}
// preserve row number for errors later
$meetings[$i]['row'] = $i + 2;
}
// normalize group fields across potentially blank rows
foreach ($meetings as $i => $meeting) {
$group = isset($meeting['group']) ? $meeting['group'] : '';
if ($group && isset($groups[$group])) {
foreach ($groups[$group] as $field => $value) {
$meetings[$i][$field] = $value;
}
}
$formatted_address = isset($meeting['formatted_address']) ? $meeting['formatted_address'] : '';
if ($formatted_address && isset($locations[$formatted_address])) {
foreach ($locations[$formatted_address] as $field => $value) {
$meetings[$i][$field] = $value;
}
}
}
// allow user-defined function to filter the meetings (for gal-aa.org)
if (function_exists('tsml_import_filter')) {
$meetings = array_filter($meetings, 'tsml_import_filter');
}
return $meetings;
}
/**
* ensure cron event is scheduled when tsml_auto_import setting changes
*
* @param null|boolean $onoff Force schedule or unscheduling cron event (default use $tsml_auto_import setting)
* @return void
*/
function tsml_import_cron_check($onoff = null)
{
global $tsml_auto_import;
$timestamp = wp_next_scheduled('tsml_auto_import');
if ((!$tsml_auto_import || false === $onoff) && $timestamp) {
wp_unschedule_event($timestamp, 'tsml_auto_import');
}
if (($tsml_auto_import || true === $onoff) && !$timestamp) {
wp_schedule_event(time(), 'ten_minutes', 'tsml_auto_import');
}
}
add_action('tsml_auto_import', 'tsml_auto_import_check');
function tsml_auto_import_check()
{
// 1. If buffer has meetings to import, pull next 10
$meetings = tsml_get_option_array('tsml_import_buffer');
if (count($meetings)) {
tsml_import_buffer_next(10);
} else {
// 2. If any data source hasn't been refreshed in last 24 hours, queue up oldest
$oldest_data_source_url = null;
$oldest_data_source_last_import = null;
$cutoff = time() - (24 * 60 * 60); // 24 hours ago
$tsml_data_sources = tsml_get_option_array('tsml_data_sources');
foreach ($tsml_data_sources as $data_source_url => $data_source) {
$last_import = intval(isset($data_source['last_import']) ? $data_source['last_import'] : 0);
if ($cutoff > $last_import && (!$oldest_data_source_last_import || $last_import < $oldest_data_source_last_import)) {
$oldest_data_source_last_import = $last_import;
$oldest_data_source_url = $data_source_url;
}
}
if ($oldest_data_source_url) {
tsml_import_data_source($oldest_data_source_url);
}
}
}