Code for Recovery is a nonprofit organization of volunteer members building technology services for recovery fellowships, such as A.A. and Al-Anon.', '12-step-meeting-list'), TSML_ALLOWED_HTML) ?>

'; foreach ($meetings as $m) { $output .= '
  • '; $output .= '' . tsml_format_day_and_time($m['day'], $m['time'], ' ', true) . ''; if ($m['id'] != $meeting_id) { $output .= '' . $m['name'] . ''; } else { $output .= $m['name']; } $output .= '
  • '; } $output .= ''; echo wp_kses($output, ['ol' => [], 'li' => [], 'a' => ['href' => []], 'span' => [], 'time' => []]); } /** * add an admin screen update message * used: tsml_import() and admin_types.php * * @param mixed $message * @param mixed $type can be success, warning, info, or error * @return void */ function tsml_alert($message, $type = 'success') { echo '

    ' . wp_kses($message, TSML_ALLOWED_HTML) . '

    '; } /** * enqueue assets for public or admin page * used: in templates and on admin_edit.php * * @return void */ function tsml_assets() { global $tsml_map, $tsml_street_only, $tsml_programs, $tsml_strings, $tsml_program, $tsml_distance_units, $tsml_defaults, $tsml_columns, $tsml_nonce, $tsml_debug; wp_enqueue_script('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], '1.9.4', true); wp_enqueue_style('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], '1.9.4'); if (is_admin()) { // dashboard page assets wp_enqueue_style('tsml_admin', plugins_url('../assets/css/admin.min.css', __FILE__), [], TSML_VERSION); wp_enqueue_script('tsml_admin', plugins_url('../assets/js/admin.min.js', __FILE__), ['jquery'], TSML_VERSION, true); wp_localize_script('tsml_admin', 'tsml', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'debug' => WP_DEBUG, 'tsml_debug' => !!$tsml_debug, 'map' => $tsml_map, 'nonce' => wp_create_nonce($tsml_nonce), ]); } else { // public page assets global $post; wp_enqueue_style('tsml_public', plugins_url('../assets/css/public.min.css', __FILE__), [], TSML_VERSION); wp_enqueue_script('jquery_validate', plugins_url('../assets/js/jquery.validate.min.js', __FILE__), ['jquery'], TSML_VERSION, true); wp_enqueue_script('tsml_public', plugins_url('../assets/js/public.min.js', __FILE__), ['jquery'], TSML_VERSION, true); wp_enqueue_script('jquery-ui-autocomplete'); wp_localize_script('tsml_public', 'tsml', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'columns' => array_keys($tsml_columns), 'days' => [ __('Sunday', '12-step-meeting-list'), __('Monday', '12-step-meeting-list'), __('Tuesday', '12-step-meeting-list'), __('Wednesday', '12-step-meeting-list'), __('Thursday', '12-step-meeting-list'), __('Friday', '12-step-meeting-list'), __('Saturday', '12-step-meeting-list'), ], 'debug' => WP_DEBUG, 'defaults' => $tsml_defaults, 'distance_units' => $tsml_distance_units, 'flags' => $tsml_programs[$tsml_program]['flags'], 'map' => $tsml_map, 'nonce' => wp_create_nonce($tsml_nonce), 'program' => empty($tsml_programs[$tsml_program]['abbr']) ? $tsml_programs[$tsml_program]['name'] : $tsml_programs[$tsml_program]['abbr'], 'street_only' => $tsml_street_only, 'strings' => $tsml_strings, 'types' => empty($tsml_programs[$tsml_program]['types']) ? [] : $tsml_programs[$tsml_program]['types'], 'meeting_id' => isset($post->ID) ? $post->ID : '', ]); } } /** * set geo boundaries from current data (for biased geocoding) * * @return void */ function tsml_bounds() { global $wpdb, $tsml_bounds; // get north & south $latitudes = $wpdb->get_row('SELECT MAX(m.meta_value) north, MIN(m.meta_value) south FROM ' . $wpdb->postmeta . ' m JOIN ' . $wpdb->posts . ' p ON p.ID = m.post_id WHERE m.meta_key = "latitude" AND p.post_type = "tsml_location"'); // get east & west $longitudes = $wpdb->get_row('SELECT MAX(m.meta_value) west, MIN(m.meta_value) east FROM ' . $wpdb->postmeta . ' m JOIN ' . $wpdb->posts . ' p ON p.ID = m.post_id WHERE m.meta_key = "longitude" AND p.post_type = "tsml_location"'); // if results, get bounding box and cache it if ($latitudes && $longitudes) { // add 25% margin to the bounds $width = ($longitudes->east - $longitudes->west) / 25; $height = ($latitudes->north - $latitudes->south) / 25; $tsml_bounds = [ 'north' => $latitudes->north + $height, 'east' => $longitudes->east + $width, 'south' => $latitudes->south - $height, 'west' => $longitudes->west - $width, ]; update_option('tsml_bounds', $tsml_bounds); } } /** * try to build a cache of meetings to help with CPU load * * @return void */ function tsml_cache_rebuild() { // flush wp object cache wp_cache_flush(); // rebuild TSML meeting cache file tsml_get_meetings([], false); } /** * calculate attendance option given types and address * called in tsml_get_meetings() * * @param mixed $types * @param mixed $approximate * @return string */ function tsml_calculate_attendance_option($types, $approximate) { $attendance_option = ''; // Handle when the types list is empty, this prevents PHP warnings if (empty($types)) { $types = []; } if (in_array('TC', $types) && in_array('ONL', $types)) { // Types has both Location Temporarily Closed and Online, which means it should be an online meeting $attendance_option = 'online'; } elseif (in_array('TC', $types)) { // Types has Location Temporarily Closed, but not online, which means it really is temporarily closed $attendance_option = 'inactive'; } elseif (in_array('ONL', $types)) { // Types has Online, but not Temp closed, which means it's a hybrid (or online) $attendance_option = 'hybrid'; if ($approximate == 'yes') { $attendance_option = 'online'; } } else { // Neither Online or Temp Closed, which means it's in person (or inactive) $attendance_option = 'in_person'; if ($approximate == 'yes') { $attendance_option = 'inactive'; } } return $attendance_option; } /** * Render svg icon icon * * @param string $icon cash|directions|email|link|phone * @return void */ function tsml_icon($icon) { ?> [ 'name' => __('Regions', '12-step-meeting-list'), 'singular_name' => __('Region', '12-step-meeting-list'), 'menu_name' => __('Regions', '12-step-meeting-list'), 'all_items' => __('All Regions', '12-step-meeting-list'), 'edit_item' => __('Edit Region', '12-step-meeting-list'), 'view_item' => __('View Region', '12-step-meeting-list'), 'update_item' => __('Update Region', '12-step-meeting-list'), 'add_new_item' => __('Add New Region', '12-step-meeting-list'), 'new_item_name' => __('New Region', '12-step-meeting-list'), 'parent_item' => __('Parent Region', '12-step-meeting-list'), 'parent_item_colon' => __('Parent Region:', '12-step-meeting-list'), 'search_items' => __('Search Regions', '12-step-meeting-list'), 'popular_items' => __('Popular Regions', '12-step-meeting-list'), 'not_found' => __('No regions found.', '12-step-meeting-list'), ], 'hierarchical' => true, 'public' => false, 'show_ui' => true, ]); register_taxonomy('tsml_district', 'tsml_group', [ 'labels' => [ 'name' => __('District', '12-step-meeting-list'), 'singular_name' => __('District', '12-step-meeting-list'), 'menu_name' => __('District', '12-step-meeting-list'), 'all_items' => __('All Districts', '12-step-meeting-list'), 'edit_item' => __('Edit District', '12-step-meeting-list'), 'view_item' => __('View District', '12-step-meeting-list'), 'update_item' => __('Update District', '12-step-meeting-list'), 'add_new_item' => __('Add New District', '12-step-meeting-list'), 'new_item_name' => __('New District', '12-step-meeting-list'), 'parent_item' => __('Parent Area', '12-step-meeting-list'), 'parent_item_colon' => __('Parent Area:', '12-step-meeting-list'), 'search_items' => __('Search Districts', '12-step-meeting-list'), 'popular_items' => __('Popular Districts', '12-step-meeting-list'), 'not_found' => __('No districts found.', '12-step-meeting-list'), ], 'hierarchical' => true, 'public' => false, 'show_ui' => true, ]); register_post_type( 'tsml_meeting', [ 'labels' => [ 'name' => __('Meetings', '12-step-meeting-list'), 'singular_name' => __('Meeting', '12-step-meeting-list'), 'not_found' => __('No meetings added yet.', '12-step-meeting-list'), 'add_new_item' => __('Add New Meeting', '12-step-meeting-list'), 'search_items' => __('Search Meetings', '12-step-meeting-list'), 'edit_item' => __('Edit Meeting', '12-step-meeting-list'), 'view_item' => __('View Meeting', '12-step-meeting-list'), ], 'supports' => ['title', 'author'], 'public' => $is_public, 'show_ui' => true, 'has_archive' => $is_public, 'menu_icon' => 'dashicons-groups', 'rewrite' => ['slug' => $tsml_slug, 'with_front' => apply_filters('tsml_meeting_with_front', true)], ] ); register_post_type( 'tsml_location', [ 'labels' => [ 'name' => __('Locations', '12-step-meeting-list'), 'singular_name' => __('Location', '12-step-meeting-list'), 'menu_name' => __('Locations', '12-step-meeting-list'), 'all_items' => __('All Locations', '12-step-meeting-list'), 'edit_item' => __('Edit Location', '12-step-meeting-list'), 'view_item' => __('View Location', '12-step-meeting-list'), 'update_item' => __('Update Location', '12-step-meeting-list'), 'add_new_item' => __('Add New Location', '12-step-meeting-list'), 'new_item_name' => __('New Location', '12-step-meeting-list'), 'parent_item' => __('Parent Location', '12-step-meeting-list'), 'parent_item_colon' => __('Parent Location:', '12-step-meeting-list'), 'search_items' => __('Search Locations', '12-step-meeting-list'), 'popular_items' => __('Popular Locations', '12-step-meeting-list'), 'not_found' => __('No locations found.', '12-step-meeting-list'), ], 'supports' => ['title'], 'public' => $is_public, 'show_ui' => false, 'has_archive' => $is_public, 'capabilities' => ['create_posts' => false], 'rewrite' => ['slug' => 'locations'], 'taxonomies' => ['tsml_region'], ] ); register_post_type( 'tsml_group', [ 'labels' => [ 'name' => __('Groups', '12-step-meeting-list'), 'singular_name' => __('Group', '12-step-meeting-list'), 'menu_name' => __('Groups', '12-step-meeting-list'), 'all_items' => __('All Groups', '12-step-meeting-list'), 'edit_item' => __('Edit Group', '12-step-meeting-list'), 'view_item' => __('View Group', '12-step-meeting-list'), 'update_item' => __('Update Group', '12-step-meeting-list'), 'add_new_item' => __('Add New Group', '12-step-meeting-list'), 'new_item_name' => __('New Group', '12-step-meeting-list'), 'parent_item' => __('Parent Group', '12-step-meeting-list'), 'parent_item_colon' => __('Parent Group:', '12-step-meeting-list'), 'search_items' => __('Search Groups', '12-step-meeting-list'), 'popular_items' => __('Popular Groups', '12-step-meeting-list'), 'not_found' => __('No groups found.', '12-step-meeting-list'), ], 'supports' => ['title'], 'public' => true, 'show_ui' => false, 'has_archive' => false, 'capabilities' => ['create_posts' => false], ] ); } /** * define custom meeting types for your area * used: theme's functions.php * * @param mixed $types * @return void */ function tsml_custom_types($types) { add_action('init', function () use ($types) { global $tsml_programs, $tsml_program; foreach ($types as $key => $value) { if (empty($value)) { // if empty, remove existing type unset($tsml_programs[$tsml_program]['types'][$key]); continue; } $tsml_programs[$tsml_program]['types'][$key] = $value; } asort($tsml_programs[$tsml_program]['types']); }); } /** * function used for debugging * * @param mixed $obj * @return never */ function tsml_dd($obj) { echo '
    ';
        print_r($obj);
        echo '
    '; exit; } /** * efficiently remove an array of post_ids * used: tsml_delete_orphans(), admin-import.php * * @param mixed $post_ids * @return void */ function tsml_delete($post_ids) { global $wpdb; //special case if ($post_ids == 'everything') { $post_ids = get_posts([ 'post_type' => ['tsml_meeting', 'tsml_location', 'tsml_group'], 'post_status' => 'any', 'fields' => 'ids', 'numberposts' => -1, ]); //when we're deleting *everything*, also delete regions & districts if ($term_ids = implode(',', $wpdb->get_col('SELECT term_id FROM ' . $wpdb->term_taxonomy . ' WHERE taxonomy IN ("tsml_district", "tsml_region")'))) { $wpdb->query('DELETE FROM ' . $wpdb->terms . ' WHERE term_id IN (' . $term_ids . ')'); $wpdb->query('DELETE FROM ' . $wpdb->term_taxonomy . ' WHERE term_id IN (' . $term_ids . ')'); } } if (empty($post_ids) || !is_array($post_ids)) { return; } // sanitize $post_ids = array_map('intval', $post_ids); $post_ids = array_unique($post_ids); $post_ids = implode(', ', $post_ids); // run deletes $wpdb->query('DELETE FROM ' . $wpdb->posts . ' WHERE ID IN (' . $post_ids . ')'); $wpdb->query('DELETE FROM ' . $wpdb->postmeta . ' WHERE post_id IN (' . $post_ids . ')'); $wpdb->query('DELETE FROM ' . $wpdb->term_relationships . ' WHERE object_id IN (' . $post_ids . ')'); // rebuild cache tsml_cache_rebuild(); } /** * efficiently deletes all orphaned locations and groups (have no meetings associated) * used: save_post filter * * @return void */ function tsml_delete_orphans() { global $wpdb; $location_ids = $wpdb->get_col('SELECT l.ID FROM ' . $wpdb->posts . ' l WHERE l.post_type = "tsml_location" AND (SELECT COUNT(*) FROM ' . $wpdb->posts . ' m WHERE m.post_type="tsml_meeting" AND m.post_parent = l.id) = 0'); $group_ids = $wpdb->get_col('SELECT g.ID FROM ' . $wpdb->posts . ' g WHERE g.post_type = "tsml_group" AND (SELECT COUNT(*) FROM ' . $wpdb->postmeta . ' m WHERE m.meta_key="group_id" AND m.meta_value = g.id) = 0'); tsml_delete(array_merge($location_ids, $group_ids)); // edge case: draft-ify locations with only unpublished meetings $location_ids = $wpdb->get_col('SELECT l.ID FROM ' . $wpdb->posts . ' l WHERE l.post_type = "tsml_location" AND (SELECT COUNT(*) FROM ' . $wpdb->posts . ' m WHERE m.post_type="tsml_meeting" AND m.post_status="publish" AND m.post_parent = l.id) = 0'); if (count($location_ids)) { $wpdb->query('UPDATE ' . $wpdb->posts . ' l SET l.post_status = "draft" WHERE ID IN (' . implode(', ', $location_ids) . ')'); } } /** * send a nice-looking email * used by tsml_ajax_feedback() and save.php (change notifications) * * @param mixed $to * @param mixed $subject * @param mixed $message * @param mixed $reply_to * @return bool */ function tsml_email($to, $subject, $message, $reply_to = false) { $headers = ['Content-Type: text/html; charset=UTF-8']; if ($reply_to) { $headers[] = 'Reply-To: ' . $reply_to; } // prepend subject as h1 $message = '

    ' . $subject . '

    ' . $message; // inline styles where necessary $message = str_replace('

    ', '

    ', $message); $message = str_replace('
    ', '
    ', $message); $message = str_replace('

    ', '

    ', $message); $message = str_replace('
    ' . $message . '
    '; return wp_mail($to, '[' . get_bloginfo('name') . '] ' . $subject, $message, $headers); } /** * display meeting list on home page (must be set to a static page) * used: by themes that want it, such as https://github.com/code4recovery/one-page-meeting-list * * @param mixed $wp_query * @return void */ function tsml_front_page($wp_query) { if (is_admin()) { return; //don't do this to inside pages } if ($wp_query->get('page_id') == get_option('page_on_front')) { $wp_query->set('post_type', 'tsml_meeting'); $wp_query->set('page_id', ''); $wp_query->is_page = 0; $wp_query->is_singular = 0; $wp_query->is_post_type_archive = 1; $wp_query->is_archive = 1; } } /** * request accurate address information from google * used: tsml_ajax_import(), tsml_ajax_geocode() * * @param mixed $address * @return mixed */ function tsml_geocode($address) { global $tsml_google_overrides; $address = stripslashes($address); // check overrides first before anything if (array_key_exists($address, $tsml_google_overrides)) { if (empty($tsml_google_overrides[$address]['approximate'])) { $tsml_google_overrides[$address]['approximate'] = 'no'; } $tsml_google_overrides[$address]['status'] = 'override'; return $tsml_google_overrides[$address]; } // check cache $addresses = tsml_get_option_array('tsml_addresses'); // if key exists && approximate is set for that address, return it if (array_key_exists($address, $addresses) && !empty($addresses[$address]['approximate'])) { $addresses[$address]['status'] = 'cache'; return $addresses[$address]; } $response = tsml_geocode_google($address); // Return if the status is error if ($response['status'] == 'error') { return $response; } // cache result $addresses[$address] = $response; $addresses[$response['formatted_address']] = $response; update_option('tsml_addresses', $addresses); return $response; } /** * call Google for geocoding of the address * * @param mixed $address * @return mixed */ function tsml_geocode_google($address) { global $tsml_curl_handle, $tsml_language, $tsml_google_overrides, $tsml_bounds; // Can't Geocode an empty address if (empty($address)) { return [ 'status' => 'error', 'reason' => 'Addres string was empty', ]; } // initialize curl handle if necessary if (!$tsml_curl_handle) { $tsml_curl_handle = curl_init(); curl_setopt_array($tsml_curl_handle, [ CURLOPT_HEADER => 0, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_SSL_VERIFYPEER => false, ]); } // form geocoding request url $options = [ 'application' => 'tsml', 'language' => $tsml_language, 'referrer' => tsml_meetings_url(), 'search' => $address, ]; if (!empty($tsml_bounds) && is_array($tsml_bounds)) { $options = array_merge($options, $tsml_bounds); } $url = TSML_GEOCODING_URL . '/api/geocode?' . http_build_query($options); // send request to google curl_setopt($tsml_curl_handle, CURLOPT_URL, $url); curl_setopt($tsml_curl_handle, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($tsml_curl_handle); // could not connect error if ($result === false) { $error = curl_error($tsml_curl_handle); tsml_log('geocode_connection_error', $error, $address); return [ 'status' => 'error', 'reason' => 'Google could not validate the address ' . $address . '. Response was ' . $error . '', ]; } // decode result $data = json_decode($result); // if over query limit, wait two seconds and retry, or then exit if ($data->status === 'OVER_QUERY_LIMIT') { sleep(2); $result = curl_exec($tsml_curl_handle); // could not connect error if ($result === false) { return [ 'status' => 'error', 'reason' => 'Google could not validate the address ' . $address . '. Response was ' . curl_error($tsml_curl_handle) . '', ]; } // decode result $data = json_decode($result); // if we're still over the limit, stop if ($data->status === 'OVER_QUERY_LIMIT') { tsml_log('geocode_error', 'OVER_QUERY_LIMIT', $address); return [ 'status' => 'error', 'reason' => 'We are over the rate limit for the Google Geocoding API.' ]; } } // if there are no results report it if ($data->status === 'ZERO_RESULTS') { tsml_log('geocode_error', 'ZERO_RESULTS', $address); return [ 'status' => 'error', 'reason' => 'Google could not validate the address ' . $address . '', ]; } // if result is otherwise bad, stop if (($data->status !== 'OK') || empty($data->results[0]->formatted_address)) { tsml_log('geocode_error', $data->status, $address); return [ 'status' => 'error', 'reason' => 'Google gave an unexpected response for address ' . $address . '. Response was

    ' . var_export($data, true) . '
    ', ]; } // check our overrides array again in case google is wrong if (array_key_exists($data->results[0]->formatted_address, $tsml_google_overrides)) { $response = $tsml_google_overrides[$data->results[0]->formatted_address]; if (empty($response['approximate'])) { $response['approximate'] = 'no'; } } else { tsml_log('geocode_success', $data->results[0]->formatted_address, $address); // start building response $response = [ 'formatted_address' => $data->results[0]->formatted_address, 'latitude' => $data->results[0]->geometry->location->lat, 'longitude' => $data->results[0]->geometry->location->lng, 'approximate' => ($data->results[0]->geometry->location_type === 'APPROXIMATE') ? 'yes' : 'no', 'city' => null, 'status' => 'geocode', ]; // get city, we might need it for the region, and we are going to cache it foreach ($data->results[0]->address_components as $component) { if (in_array('locality', $component->types)) { $response['city'] = $component->short_name; } } } return $response; } /** * return spelled-out meeting types * called from save.php (updates) and archive-meetings.php (display) * * @param mixed $types * @return string | void */ function tsml_meeting_types($types) { global $tsml_programs, $tsml_program; if (empty($tsml_programs[$tsml_program]['types'])) { return; } $return = []; foreach ($types as $type) { if (array_key_exists($type, $tsml_programs[$tsml_program]['types'])) { $return[] = $tsml_programs[$tsml_program]['types'][$type]; } } sort($return); return implode(', ', $return); } /** * return an array of languages, with legacy code substitutions per-program * used: variables.php, meeting-edit.php */ function tsml_languages($types = []) { global $tsml_program; // https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes $languages = [ 'AF' => __('Afrikaans', '12-step-meeting-list'), 'AM' => __('Amharic', '12-step-meeting-list'), 'AR' => __('Arabic', '12-step-meeting-list'), 'BG' => __('Bulgarian', '12-step-meeting-list'), 'DA' => __('Danish', '12-step-meeting-list'), 'DE' => __('German', '12-step-meeting-list'), 'EL' => __('Greek', '12-step-meeting-list'), 'EN' => __('English', '12-step-meeting-list'), 'ES' => __('Spanish', '12-step-meeting-list'), 'FA' => __('Persian', '12-step-meeting-list'), 'FI' => __('Finnish', '12-step-meeting-list'), 'FR' => __('French', '12-step-meeting-list'), 'HE' => __('Hebrew', '12-step-meeting-list'), 'HI' => __('Hindi', '12-step-meeting-list'), 'HR' => __('Croatian', '12-step-meeting-list'), 'HU' => __('Hungarian', '12-step-meeting-list'), 'IS' => __('Icelandic', '12-step-meeting-list'), 'IT' => __('Italian', '12-step-meeting-list'), 'JA' => __('Japanese', '12-step-meeting-list'), 'KA' => __('Georgian', '12-step-meeting-list'), 'KO' => __('Korean', '12-step-meeting-list'), 'LT' => __('Lithuanian', '12-step-meeting-list'), 'ML' => __('Malayalam', '12-step-meeting-list'), 'MT' => __('Maltese', '12-step-meeting-list'), 'NE' => __('Nepali', '12-step-meeting-list'), 'NL' => __('Dutch', '12-step-meeting-list'), 'NO' => __('Norwegian', '12-step-meeting-list'), 'PA' => __('Punjabi', '12-step-meeting-list'), 'PL' => __('Polish', '12-step-meeting-list'), 'PT' => __('Portuguese', '12-step-meeting-list'), 'RU' => __('Russian', '12-step-meeting-list'), 'SK' => __('Slovak', '12-step-meeting-list'), 'SL' => __('Slovenian', '12-step-meeting-list'), 'SV' => __('Swedish', '12-step-meeting-list'), 'TH' => __('Thai', '12-step-meeting-list'), 'TL' => __('Tagalog', '12-step-meeting-list'), 'TR' => __('Turkish', '12-step-meeting-list'), 'UK' => __('Ukrainian', '12-step-meeting-list'), ]; // substitutions $substitutions = [ 'aca' => [ 'ES' => 'SP', ], 'al-anon' => [ 'ES' => 'S', ], 'aa' => [ 'ES' => 'S', 'IT' => 'ITA', 'KO' => 'KOR', 'PA' => 'PUN', 'PL' => 'POL', 'PT' => 'POR', 'RU' => 'RUS', 'TR' => 'TUR', ], 'ca' => [ 'ES' => 'S', 'IT' => 'ITA', 'KO' => 'KOR', 'PA' => 'PUN', 'PL' => 'POL', 'PT' => 'POR', 'RU' => 'RUS', ], 'cma' => [ 'ES' => 'S', ], 'coda' => [ 'ES' => 'S', ], 'eda' => [ 'ES' => 'S', 'IT' => 'ITA', ], 'ga' => [ 'ES' => 'S', 'IT' => 'ITA', 'KO' => 'KOR', 'PA' => 'PUN', 'PL' => 'POL', 'PT' => 'POR', 'RU' => 'RUS', ], 'slaa' => [ 'ES' => 'S', ], 'sia' => [ 'ES' => 'S', 'IT' => 'ITA', 'KO' => 'KOR', 'PA' => 'PUN', 'PL' => 'POL', 'PT' => 'POR', 'RU' => 'RUS', 'UK' => 'UA', ], ]; // if the program has no types, return the languages if (array_key_exists($tsml_program, $substitutions)) { foreach ($substitutions[$tsml_program] as $key => $value) { if (array_key_exists($key, $languages)) { $languages[$value] = $languages[$key]; unset($languages[$key]); } } } foreach ($languages as $key => $value) { $types[$key] = $value; } asort($types); return $types; } /** * Render an anchor tag with a class * @deprecated this function is no longer used. it will be removed in the future * @param mixed $url * @param mixed $string * @param mixed $exclude * @param mixed $class * @return string */ function tsml_link($url, $string, $exclude = '', $class = false) { $appends = $_GET; if (array_key_exists($exclude, $appends)) { unset($appends[$exclude]); } if (!empty($appends)) { $url .= strstr($url, '?') ? '&' : '?'; $url .= http_build_query($appends, '', '&'); } $return = ''; return $return; } /** * return string link with current query string appended * * @param string $url * @param mixed $exclude * @return string */ function tsml_link_url($url, $exclude = '') { $appends = $_GET; if (array_key_exists($exclude, $appends)) { unset($appends[$exclude]); } if (!empty($appends)) { $url .= strstr($url, '?') ? '&' : '?'; $url .= http_build_query($appends, '', '&'); } return $url; } /** * link to meetings page with parameters * used: admin_import.php, admin_settings.php, archive-meetings.php, init.php, widgets.php, tsml_geocode_google() * * @param mixed $parameters * @return string */ function tsml_meetings_url($parameters = []) { $url = get_post_type_archive_link('tsml_meeting'); if (empty($url)) { $url = get_home_url(); } $url = add_query_arg($parameters, $url); return $url; } /** * convert line breaks in plain text to HTML paragraphs * used: save.php in lieu of nl2br() * * @param mixed $string * @return string */ function tsml_paragraphs($string) { $paragraphs = ''; foreach (explode("\n", trim($string)) as $line) { if ($line = trim($line)) { $paragraphs .= '

    ' . $line . '

    '; } } return $paragraphs; } /** * boolean whether current program has types * used: meeting edit screen, meeting save * * @return bool */ function tsml_program_has_types() { global $tsml_programs, $tsml_program; return !empty($tsml_programs[$tsml_program]['types']); } /** * exit if user does not have permission to edit meetings * * @return void */ function tsml_require_meetings_permission() { if (!current_user_can(TSML_MEETINGS_PERMISSION)) { // translators: %s is the permission required wp_die(wp_kses(sprintf(__('You do not have sufficient permissions to access this page (%s).', '12-step-meeting-list'), TSML_MEETINGS_PERMISSION), TSML_ALLOWED_HTML)); } } /** * exit if user does not have permission to edit settings * * @return void */ function tsml_require_settings_permission() { if (!current_user_can(TSML_SETTINGS_PERMISSION)) { // translators: %s is the permission required wp_die(wp_kses(sprintf(__('You do not have sufficient permissions to access this page (%s).', '12-step-meeting-list'), TSML_SETTINGS_PERMISSION), TSML_ALLOWED_HTML)); } } /** * set an option with the currently-used types * used tsml_import() and save.php * * @return void */ function tsml_update_types_in_use() { global $tsml_types_in_use, $wpdb; // shortcut to getting all meta values without getting all posts first $types = $wpdb->get_col('SELECT m.meta_value FROM ' . $wpdb->postmeta . ' m JOIN ' . $wpdb->posts . ' p ON m.post_id = p.id WHERE p.post_type = "tsml_meeting" AND m.meta_key = "types" AND p.post_status = "publish"'); // main array $all_types = []; // loop through results and append to main array foreach ($types as $type) { $type = unserialize($type); if (is_array($type)) { $all_types = array_merge($all_types, $type); } } // update global variable $tsml_types_in_use = array_unique($all_types); // set option value update_option('tsml_types_in_use', $tsml_types_in_use); } /** * sanitize a value * used: save.php * * @param mixed $type * @param mixed $value * @return array|string|null */ function tsml_sanitize($type, $value) { if ($type == 'url') { return esc_url_raw($value, ['http', 'https']); } elseif ($type == 'date') { return date('Y-m-d', strtotime($value)); } elseif ($type == 'time') { return date('H:i', strtotime($value)); } elseif ($type == 'phone') { return preg_replace('/[^0-9,+#]/', '', $value); } return sanitize_text_field($value); } /** * sanitize multi-line text * used tsml_import() and save.php * @param string $value * @return string */ function tsml_sanitize_text_area($value) { return implode("\n", array_map('sanitize_text_field', explode("\n", trim($value)))); } /** * does a string end with another string * used: save.php * * @param mixed $string * @param mixed $end * @return bool */ function tsml_string_ends($string, $end) { $length = strlen($end); if (!$length) { return true; } return (substr($string, -$length) === $end); } /** * tokenize string for the typeaheads * used: ajax functions * @param mixed $string * @return array */ function tsml_string_tokens($string) { // shorten words that have quotes in them instead of splitting them $string = html_entity_decode($string); $string = str_replace("'", '', $string); $string = str_replace('’', '', $string); // remove everything that's not a letter or a number $string = preg_replace("/[^a-zA-Z 0-9]+/", ' ', $string); // return array return array_values(array_unique(array_filter(explode(' ', $string)))); } /** * Implodes given array $types into lower-cased class names, prefixing each with $prefix * * @param string[] $types * @param string $prefix * @return string */ function tsml_to_css_classes($types, $prefix = 'type-') { if (!$types) { return ''; } $types = array_map('strtolower', $types); return $prefix . implode(' ' . $prefix, $types); } /** * Sanitizes a string for sorting purposes. Similar to sanitize_title(), but uses Unicode regular expressions to support multiple languages. * * @param string $string * @return string */ function tsml_sanitize_data_sort($string) { global $tsml_sanitize_data_sort_regexps; // Populate regex array only once if (!isset($tsml_sanitize_data_sort_regexps)) { $tsml_sanitize_data_sort_regexps = [ ['/<[^>]+>/', ''], // Strip HTML Tags ['/[&\'"<>]+/', ''], // Strip unsupported chars ['/[\\/\\.\\p{Zs}\\p{Pd}]/u', '-'], // Change forward slashes, periods, spaces and dashes to dash (Unicode) ['/[^\\p{L}\\p{N}\\p{M}\-]+/u', ''], // Remove any Unicode char that is not an alpha-numeric, mark character or dash ['/\-+/', '-'], // Convert runs of dashes into a single dash ['/^\-|\-$/', ''] // Strip trailing/leading dash ]; } // Convert all html entities to chars so encodings are uniform $t = html_entity_decode($string); // Do regex-based sanitization foreach ($tsml_sanitize_data_sort_regexps as $a) { $t = preg_replace($a[0], $a[1], $t); } // Unicode-aware lowercase of characters in string if (function_exists('mb_strtolower')) { return mb_strtolower($t); } else { return strtolower($t); } } /** * called by register_activation_hook in admin_import * @return void */ function tsml_activate_data_source_scan() { // Use wp_next_scheduled to check if the event is already scheduled $timestamp = wp_next_scheduled('tsml_scan_data_source'); // If $timestamp is false schedule scan since it hasn't been done previously if ($timestamp == false) { // Schedule the event for right now, then to reoccur daily using the hook 'tsml_scan_data_source' wp_schedule_event(time(), 'daily', 'tsml_scan_data_source'); } } // function: scans passed data source url looking for recent updates // used: fired by cron job tsml_scan_data_source // todo - think about what the right place for this is add_action('tsml_scan_data_source', function ($data_source_url) { $data_source_name = null; $data_source_parent_region_id = -1; $data_source_count_meetings = 0; $tsml_notification_addresses = tsml_get_option_array('tsml_notification_addresses'); $tsml_data_sources = tsml_get_option_array('tsml_data_sources'); $data_source_count_meetings = (int) $tsml_data_sources[$data_source_url]['count_meetings']; if (empty($tsml_notification_addresses) || $data_source_count_meetings !== 0 || !array_key_exists($data_source_url, $tsml_data_sources)) { return; } $data_source_name = $tsml_data_sources[$data_source_url]['name']; $data_source_parent_region_id = $tsml_data_sources[$data_source_url]['parent_region_id']; // try fetching $response = wp_safe_remote_get($data_source_url, ['timeout' => 30, 'sslverify' => false]); if (empty($response['body']) || !($body = json_decode($response['body'], true))) { return; } // allow reformatting as necessary $meetings = tsml_import_sanitize_meetings($body, $data_source_url, $data_source_parent_region_id); // check import feed for changes and return array summing up changes detected $change_log = tsml_import_get_changed_meetings($meetings, $data_source_url); // send email notifying admins that this data source needs updating if (is_array($change_log) && count($change_log)) { // translators: %s is the name of the data source $message = '

    ' . sprintf(__('Please sign in to your website and refresh the %s feed on the Import & Export page.', '12-step-meeting-list'), $data_source_name) . '


    '; $message .= tsml_import_build_change_report($change_log); $import_page_url = admin_url('/edit.php?post_type=tsml_meeting&page=import'); $button_text = __('Go to Import & Export page', '12-step-meeting-list'); $message .= '
    ' . $button_text . ''; $subject = __('Data Source Changes Detected', '12-step-meeting-list'); tsml_email($tsml_notification_addresses, $subject, $message); } }, 10, 1); /** * Creates and configures cron job to run a scheduled data source scan * * @param mixed $data_source_url * @param mixed $data_source_name * @return void */ function tsml_schedule_import_scan($data_source_url, $data_source_name) { $timestamp = tsml_strtotime('tomorrow midnight'); // Use tsml_strtotime to incorporate local site timezone with UTC. // Get the timestamp for the next event when found. $ts = wp_next_scheduled("tsml_scan_data_source", array($data_source_url)); if ($ts) { $mydisplaytime = tsml_date_localised(get_option('date_format') . ' ' . get_option('time_format'), $ts); // Use tsml_date_localised to convert to specified format with local site timezone included. tsml_alert("The $data_source_name data source's next scheduled run is $mydisplaytime. You can adjust the recurrences and the times that the job ('tsml_scan_data_source') runs with the WP_Crontrol plugin."); } else { // When adding a data source we schedule its daily cron job register_activation_hook(__FILE__, 'tsml_activate_data_source_scan'); // Schedule the refresh if (wp_schedule_event($timestamp, "daily", "tsml_scan_data_source", array($data_source_url)) === false) { tsml_alert("$data_source_name data source scan scheduling failed!"); } else { $mydisplaytime = tsml_date_localised(get_option('date_format') . ' ' . get_option('time_format'), $timestamp); // Use tsml_date_localised to convert to specified format with local site timezone included. tsml_alert("The $data_source_name data source's next scheduled run is $mydisplaytime. You can adjust the recurrences and the times that the job ('tsml_scan_data_source') runs with the WP_Crontrol plugin."); } } } /** * incorporates wp timezone into php's StrToTime() function * used: here, admin-import.php * * @param mixed $str * @return string */ function tsml_strtotime($str) { // This function behaves a bit like PHP's StrToTime() function, but taking into account the Wordpress site's timezone // CAUTION: It will throw an exception when it receives invalid input - please catch it accordingly // From https://mediarealm.com.au/ $tz_string = get_option('timezone_string'); $tz_offset = get_option('gmt_offset', 0); if (!empty($tz_string)) { // If site timezone option string exists, use it $timezone = $tz_string; } elseif ($tz_offset == 0) { // get UTC offset, if it isn’t set then return UTC $timezone = 'UTC'; } else { $timezone = $tz_offset; if (substr($tz_offset, 0, 1) != "-" && substr($tz_offset, 0, 1) != "+" && substr($tz_offset, 0, 1) != "U") { $timezone = "+" . $tz_offset; } } $datetime = new DateTime($str, new DateTimeZone($timezone)); return $datetime->format('U'); } /** * incorporates wp timezone into php's date() function * used: here, admin-import.php * * @param mixed $format * @param mixed $timestamp * @return string */ function tsml_date_localised($format, $timestamp = null) { // This function behaves a bit like PHP's Date() function, but taking into account the Wordpress site's timezone // CAUTION: It will throw an exception when it receives invalid input - please catch it accordingly // From https://mediarealm.com.au/ $tz_string = get_option('timezone_string'); $tz_offset = get_option('gmt_offset', 0); if (!empty($tz_string)) { // If site timezone option string exists, use it $timezone = $tz_string; } elseif ($tz_offset == 0) { // get UTC offset, if it isn’t set then return UTC $timezone = 'UTC'; } else { $timezone = $tz_offset; if (substr($tz_offset, 0, 1) != "-" && substr($tz_offset, 0, 1) != "+" && substr($tz_offset, 0, 1) != "U") { $timezone = "+" . $tz_offset; } } if ($timestamp === null) { $timestamp = time(); } $datetime = new DateTime(); $datetime->setTimestamp($timestamp); $datetime->setTimezone(new DateTimeZone($timezone)); return $datetime->format($format); } /** * Compares content of an import meeting against local meeting * @param array $meeting_old Old version of meeting (local version) * @param array $meeting_new New version of meeting (imported version) * @param boolean $compare_import [default false] Imported meetings use source fields for comparison * @param boolean $translate_fields [default false] Translate system fields to field labels * @return array|null */ function tsml_compare_meetings($meeting_old, $meeting_new, $compare_import = false, $translate_fields = false) { global $tsml_export_columns, $tsml_source_fields_map, $tsml_entity_fields, $tsml_array_fields; $meeting_old = (array) $meeting_old; $meeting_new = (array) $meeting_new; // update local meeting with stored source field values if ($compare_import) { foreach ($tsml_source_fields_map as $source_field => $field) { if (isset($meeting_old[$source_field])) { $meeting_old[$field] = $meeting_old[$source_field]; } else { unset($meeting_old[$field]); } } } $compare_fields = array_merge(array_keys($tsml_export_columns), $tsml_entity_fields); $compare_fields = array_diff( $compare_fields, // these fields are internal fields, not content fields for comparison explode(',', 'id,slug,author,data_source,data_source_name,updated,latitude,longitude') ); // normalize meetings for comparison $normalized_meetings = array(); foreach (array($meeting_old, $meeting_new) as $index => $meeting) { $normalized_meetings[$index] = array(); foreach ($compare_fields as $field) { $value = isset($meeting[$field]) ? $meeting[$field] : ''; if (in_array($field, $tsml_array_fields, true)) { $value = empty($value) ? [] : ((array) $value); } // import meeting: post_title <=> name if (!$value && 'name' === $field) { $value = isset($meeting['post_title']) ? $meeting['post_title'] : ''; } if (is_object($value) || is_array($value)) { $value = json_encode($value); } elseif (null !== $value) { $value = trim(htmlspecialchars_decode(html_entity_decode(strval($value)), ENT_QUOTES)); } else { $value = ''; } $normalized_meetings[$index][$field] = $value; } } // if 'group' is blank on both, we don't compare group_notes or district if (!$normalized_meetings[0]['group'] && !$normalized_meetings[1]['group']) { $compare_fields = array_diff($compare_fields, ['district', 'group_notes']); } $diff_fields = array(); foreach ($compare_fields as $field) { if ($normalized_meetings[0][$field] !== $normalized_meetings[1][$field]) { $diff_fields[] = ($translate_fields && isset($tsml_export_columns[$field])) ? $tsml_export_columns[$field] : $field; } } return count($diff_fields) ? $diff_fields : null; } /** * Return hash for imported meeting, for later comparison * * @param [array] $meeting * @return string */ function tsml_get_import_hash($meeting) { $remove_fields = explode(',', ',row,import_hash'); if (is_array($meeting)) { foreach ($meeting as $field => $value) { if (in_array(strtolower($field), $remove_fields)) { unset($meeting[$field]); } } } return md5(serialize($meeting)); } function tsml_header() { if (function_exists('wp_is_block_theme') && wp_is_block_theme()) { include(TSML_PATH . '/templates/header.php'); } else { get_header(); } } function tsml_footer() { if (function_exists('wp_is_block_theme') && wp_is_block_theme()) { include(TSML_PATH . '/templates/footer.php'); } else { get_footer(); } } /** * Redirect legacy query parameters to TSML UI's url structure */ function tsml_redirect_legacy_query_params() { global $tsml_program, $tsml_programs; $replacements = []; if (isset($_GET['tsml-attendance_option'])) { if ($_GET['tsml-attendance_option'] === 'active') { $replacements['type'][] = 'active'; } elseif ($_GET['tsml-attendance_option'] === 'in_person') { $replacements['type'][] = 'in-person'; } elseif ($_GET['tsml-attendance_option'] === 'online') { $replacements['type'][] = 'online'; } } if (isset($_GET['tsml-day'])) { $days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; if (array_key_exists($_GET['tsml-day'], $days)) { $replacements['weekday'] = $days[$_GET['tsml-day']]; } } if (isset($_GET['tsml-mode'])) { $replacements['mode'] = $_GET['tsml-mode']; if (isset($_GET['tsml-distance'])) { $replacements['distance'] = $_GET['tsml-distance']; } else { $replacements['distance'] = 10; } } if (isset($_GET['tsml-query'])) { $replacements['search'] = $_GET['tsml-query']; } if (isset($_GET['tsml-region'])) { $replacements['region'] = $_GET['tsml-region']; } if (isset($_GET['tsml-time'])) { $replacements['time'] = $_GET['tsml-time']; } if (isset($_GET['tsml-type'])) { $types = explode(',', $_GET['tsml-type']); foreach ($types as $type) { if (array_key_exists($type, $tsml_programs[$tsml_program]['types'])) $replacements['type'][] = sanitize_title($tsml_programs[$tsml_program]['types'][$type]); } } if (isset($replacements['type'])) { $replacements['type'] = implode('/', $replacements['type']); } if (count($replacements) > 0) { wp_redirect(tsml_meetings_url($replacements)); } }