'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$variables = isset( $data['variables'] ) ? $data['variables'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_CACHE_KEY, $variables, $cache_expiry );
}
return $variables;
}
/**
* Get a single site variable.
*
* @since 1.0.0
* @param string $key Variable key (e.g., 'phone_primary', 'facebook_url').
* @param string $default Default value if not found.
* @return string Variable value.
*/
function a1tools_get_variable( $key, $default = '' ) {
$variables = a1tools_get_site_variables();
if ( null === $variables || ! isset( $variables[ $key ] ) ) {
return $default;
}
return $variables[ $key ];
}
/**
* Clear the site variables cache.
*
* @since 1.0.0
* @return void
*/
function a1tools_clear_cache() {
delete_transient( A1TOOLS_CACHE_KEY );
delete_transient( A1TOOLS_STORES_CACHE_KEY );
delete_transient( A1TOOLS_FRANCHISES_CACHE_KEY );
delete_transient( A1TOOLS_CITY_PAGES_CACHE_KEY );
delete_transient( A1TOOLS_SERVICES_CACHE_KEY );
delete_transient( A1TOOLS_REVIEWS_CACHE_KEY );
delete_transient( A1TOOLS_BEFORE_AFTER_CACHE_KEY );
delete_transient( A1TOOLS_TEAM_CACHE_KEY );
delete_transient( A1TOOLS_FAQ_CACHE_KEY );
}
/**
* Get group store locations from the A1 Tools API.
*
* Fetches store data for the group that this site belongs to.
* Results are cached using transients.
*
* @since 1.7.0
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Array of stores or null on failure.
*/
function a1tools_get_group_stores( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_STORES_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_stores_api_url', A1TOOLS_DEFAULT_STORES_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$stores = isset( $data['stores'] ) ? $data['stores'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_STORES_CACHE_KEY, $stores, $cache_expiry );
}
return $stores;
}
/**
* Get franchise locations from the A1 Tools API.
*
* Fetches franchise data for this specific site.
* Results are cached using transients.
*
* @since 1.7.1
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Array of franchises or null on failure.
*/
function a1tools_get_site_franchises( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_FRANCHISES_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_franchises_api_url', A1TOOLS_DEFAULT_FRANCHISES_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$franchises = isset( $data['franchises'] ) ? $data['franchises'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_FRANCHISES_CACHE_KEY, $franchises, $cache_expiry );
}
return $franchises;
}
/**
* Get city page data from the A1 Tools API.
*
* Fetches city-specific phone/address data for this site's WordPress city pages.
* Results are cached using transients and keyed by wp_page_id for O(1) shortcode lookup.
*
* @since 1.7.9
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Associative array keyed by wp_page_id, or null on failure.
*/
function a1tools_get_site_city_pages( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_CITY_PAGES_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_city_pages_api_url', A1TOOLS_DEFAULT_CITY_PAGES_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
// city_pages is already keyed by wp_page_id from the API
$city_pages = isset( $data['city_pages'] ) ? $data['city_pages'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_CITY_PAGES_CACHE_KEY, $city_pages, $cache_expiry );
}
return $city_pages;
}
/**
* Get ALL WordPress city pages by querying the local WP database directly.
*
* City pages are identified by author display names containing "City"
* (e.g., "Single City", "Location City"), excluding "SEO City" authors.
* This returns every published city page regardless of whether it has
* saved data in the A1 Tools API.
*
* Uses $wpdb direct queries and WP_Query for maximum reliability across
* all WordPress versions and configurations.
*
* @since 1.8.2
* @return array Array of city pages keyed by WP page ID.
*/
function a1tools_get_wp_city_pages() {
static $cached = null;
if ( null !== $cached ) {
return $cached;
}
global $wpdb;
// Direct DB query for city authors — most reliable across all WP versions.
// Match display_name containing "city" but NOT "seo" (excludes "SEO City" authors).
$authors = $wpdb->get_results(
"SELECT ID, display_name FROM {$wpdb->users}
WHERE LOWER(display_name) LIKE '%city%'
AND LOWER(display_name) NOT LIKE '%seo%'"
);
if ( empty( $authors ) ) {
$cached = array();
return $cached;
}
$city_author_ids = array();
$author_names = array();
foreach ( $authors as $author ) {
$city_author_ids[] = (int) $author->ID;
$author_names[ (int) $author->ID ] = $author->display_name;
}
// Use WP_Query for pages — handles large result sets reliably.
$query = new WP_Query(
array(
'post_type' => 'page',
'author__in' => $city_author_ids,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
'no_found_rows' => true, // Skip counting for performance.
)
);
$result = array();
if ( $query->have_posts() ) {
foreach ( $query->posts as $page ) {
$author_id = (int) $page->post_author;
$author_name = isset( $author_names[ $author_id ] ) ? $author_names[ $author_id ] : '';
$page_type = ( false !== stripos( $author_name, 'location' ) ) ? 'location_city' : 'single_city';
$result[ $page->ID ] = array(
'wp_page_id' => $page->ID,
'page_title' => $page->post_title,
'page_type' => $page_type,
'author_name' => $author_name,
'group_name' => null,
'group_order' => 0,
);
}
}
wp_reset_postdata();
$cached = $result;
return $cached;
}
/**
* Get ALL WordPress service pages by querying the local WP database directly.
*
* Service pages are identified by author display names containing "large service"
* (e.g., "Large Service"), excluding "SEO" authors.
* This returns every published service page regardless of whether it has
* saved data in the A1 Tools API.
*
* @since 1.9.0
* @return array Array of service pages keyed by WP page ID.
*/
function a1tools_get_wp_service_pages() {
static $cached = null;
if ( null !== $cached ) {
return $cached;
}
global $wpdb;
// Direct DB query for service authors — most reliable across all WP versions.
// Match display_name containing "large service" but NOT "seo".
$authors = $wpdb->get_results(
"SELECT ID, display_name FROM {$wpdb->users}
WHERE LOWER(display_name) LIKE '%large service%'
AND LOWER(display_name) NOT LIKE '%seo%'"
);
if ( empty( $authors ) ) {
$cached = array();
return $cached;
}
$service_author_ids = array();
$author_names = array();
foreach ( $authors as $author ) {
$service_author_ids[] = (int) $author->ID;
$author_names[ (int) $author->ID ] = $author->display_name;
}
// Use WP_Query for pages — handles large result sets reliably.
$query = new WP_Query(
array(
'post_type' => 'page',
'author__in' => $service_author_ids,
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'title',
'order' => 'ASC',
'no_found_rows' => true, // Skip counting for performance.
)
);
$result = array();
if ( $query->have_posts() ) {
foreach ( $query->posts as $page ) {
$author_id = (int) $page->post_author;
$author_name = isset( $author_names[ $author_id ] ) ? $author_names[ $author_id ] : '';
$result[ $page->ID ] = array(
'wp_page_id' => $page->ID,
'page_title' => $page->post_title,
'author_name' => $author_name,
);
}
}
wp_reset_postdata();
$cached = $result;
return $cached;
}
/**
* Get service data from the A1 Tools API.
*
* Fetches service-specific data for this site's WordPress service pages.
* Results are cached using transients and keyed by wp_page_id for O(1) shortcode lookup.
*
* @since 1.9.0
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Associative array keyed by wp_page_id, or null on failure.
*/
function a1tools_get_site_services( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_SERVICES_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_services_api_url', A1TOOLS_DEFAULT_SERVICES_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$services = isset( $data['services'] ) ? $data['services'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_SERVICES_CACHE_KEY, $services, $cache_expiry );
}
return $services;
}
/**
* Get group reviews from the A1 Tools API.
*
* Fetches review data for the group that this site belongs to.
* Results are cached using transients.
*
* @since 1.9.0
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Array of reviews or null on failure.
*/
function a1tools_get_group_reviews( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_REVIEWS_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_reviews_api_url', A1TOOLS_DEFAULT_REVIEWS_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$reviews = isset( $data['reviews'] ) ? $data['reviews'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_REVIEWS_CACHE_KEY, $reviews, $cache_expiry );
}
return $reviews;
}
/**
* Get group before/after items from the A1 Tools API.
*
* Fetches before/after comparison data for the group that this site belongs to.
* Results are cached using transients.
*
* @since 1.9.0
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Array of before/after items or null on failure.
*/
function a1tools_get_group_before_after( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_BEFORE_AFTER_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_before_after_api_url', A1TOOLS_DEFAULT_BEFORE_AFTER_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$items = isset( $data['items'] ) ? $data['items'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_BEFORE_AFTER_CACHE_KEY, $items, $cache_expiry );
}
return $items;
}
/**
* Get group team members from the A1 Tools API.
*
* Fetches team member data for the group that this site belongs to.
* Results are cached using transients.
*
* @since 1.9.0
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Array of team members or null on failure.
*/
function a1tools_get_group_team( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_TEAM_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_team_api_url', A1TOOLS_DEFAULT_TEAM_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$members = isset( $data['members'] ) ? $data['members'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_TEAM_CACHE_KEY, $members, $cache_expiry );
}
return $members;
}
/**
* Get group FAQ items from the A1 Tools API.
*
* Fetches FAQ data for the group that this site belongs to.
* Results are cached using transients.
*
* @since 1.9.0
* @param bool $force_refresh Whether to bypass cache.
* @return array|null Array of FAQ items or null on failure.
*/
function a1tools_get_group_faq( $force_refresh = false ) {
// Check cache first.
if ( ! $force_refresh ) {
$cached = get_transient( A1TOOLS_FAQ_CACHE_KEY );
if ( false !== $cached ) {
return $cached;
}
}
$site_url = site_url();
$api_base_url = apply_filters( 'a1tools_faq_api_url', A1TOOLS_DEFAULT_FAQ_API_URL );
$api_url = add_query_arg(
array(
'action' => 'public',
'url' => $site_url,
),
$api_base_url
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 10,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return null;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || true !== $data['success'] ) {
return null;
}
$faqs = isset( $data['faqs'] ) ? $data['faqs'] : array();
// Cache the result.
$cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY );
if ( $cache_expiry > 0 ) {
set_transient( A1TOOLS_FAQ_CACHE_KEY, $faqs, $cache_expiry );
}
return $faqs;
}
// ============================================================================
// SHORTCODES
// ============================================================================
/**
* Process A1 Tools shortcodes inside HTML attributes (href, src, etc.).
*
* WordPress does not run do_shortcode() inside HTML attributes. This filter
* finds occurrences like href="tel:[a1tools_var key="phone_primary"]"
* or href="mailto:[a1tools_var ...]" and resolves them before normal rendering.
*
* @since 1.5.0
* @param string $content Post content.
* @return string Content with shortcodes resolved inside attributes.
*/
function a1tools_process_shortcodes_in_attributes( $content ) {
// Match href="...anything with [a1tools_ shortcode..." (handles HTML-encoded quotes too)
$content = preg_replace_callback(
'/(?<=href=["\'])([^"\']*\[a1tools_[^\]]*\][^"\']*)/i',
function ( $matches ) {
// Decode HTML entities so shortcode parser can read the attributes
$decoded = html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' );
// Process any shortcodes found inside the href value
return do_shortcode( $decoded );
},
$content
);
return $content;
}
add_filter( 'the_content', 'a1tools_process_shortcodes_in_attributes', 7 );
add_filter( 'widget_text', 'a1tools_process_shortcodes_in_attributes', 7 );
/**
* Shortcode: [a1tools_var key="phone_primary"]
* Outputs a site variable value.
*
* @since 1.0.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_var( $atts ) {
$atts = shortcode_atts(
array(
'key' => '',
'link' => 'false',
'target' => '_blank',
'class' => '',
'default' => '',
),
$atts,
'a1tools_var'
);
if ( empty( $atts['key'] ) ) {
return '';
}
$value = a1tools_get_variable( $atts['key'], $atts['default'] );
if ( empty( $value ) ) {
return esc_html( $atts['default'] );
}
// Wrap in link if requested and value is a URL.
if ( 'true' === $atts['link'] && filter_var( $value, FILTER_VALIDATE_URL ) ) {
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
$rel_attr = '_blank' === $atts['target'] ? ' rel="noopener noreferrer"' : '';
return '' . esc_html( $value ) . '';
}
// Wrap in span if class provided.
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $value ) . '';
}
return esc_html( $value );
}
add_shortcode( 'a1tools_var', 'a1tools_shortcode_var' );
/**
* Shortcode: [a1tools_website_url]
* Outputs the website URL as text or a clickable link.
*
* @since 1.5.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_website_url( $atts ) {
$atts = shortcode_atts(
array(
'link' => 'false',
'target' => '_blank',
'class' => '',
'text' => '',
),
$atts,
'a1tools_website_url'
);
$url = home_url( '/' );
// If link="true", output as a clickable link.
if ( 'true' === $atts['link'] ) {
$link_text = ! empty( $atts['text'] ) ? esc_html( $atts['text'] ) : esc_html( $url );
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
$rel_attr = '_blank' === $atts['target'] ? ' rel="noopener noreferrer"' : '';
return '' . $link_text . '';
}
// Plain text output.
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $url ) . '';
}
return esc_html( $url );
}
add_shortcode( 'a1tools_website_url', 'a1tools_shortcode_website_url' );
/**
* Shortcode: [a1tools_address format="full"]
* Outputs formatted address.
*
* @since 1.0.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_address( $atts ) {
$atts = shortcode_atts(
array(
'format' => 'full',
'separator' => '
',
),
$atts,
'a1tools_address'
);
$vars = a1tools_get_site_variables();
if ( ! $vars ) {
return '';
}
$parts = array();
switch ( $atts['format'] ) {
case 'street':
$parts = array_filter(
array(
isset( $vars['address_line1'] ) ? $vars['address_line1'] : '',
isset( $vars['address_line2'] ) ? $vars['address_line2'] : '',
)
);
break;
case 'city_state_zip':
$city_state = array_filter(
array(
isset( $vars['city'] ) ? $vars['city'] : '',
isset( $vars['state'] ) ? $vars['state'] : '',
)
);
$zip = isset( $vars['zip'] ) ? $vars['zip'] : '';
$parts = array( implode( ', ', $city_state ) . ' ' . $zip );
break;
case 'full':
default:
$city = isset( $vars['city'] ) ? $vars['city'] : '';
$state = isset( $vars['state'] ) ? $vars['state'] : '';
$zip = isset( $vars['zip'] ) ? $vars['zip'] : '';
$state_zip = trim( $state . ' ' . $zip );
$city_line = $city ? $city . ( $state_zip ? ', ' . $state_zip : '' ) : $state_zip;
$parts = array_filter(
array(
isset( $vars['address_line1'] ) ? $vars['address_line1'] : '',
isset( $vars['address_line2'] ) ? $vars['address_line2'] : '',
$city_line,
isset( $vars['country'] ) ? $vars['country'] : '',
)
);
break;
}
// Handle separator - allow
for HTML output, otherwise escape.
$separator = $atts['separator'];
if ( '
' === $separator || '
' === $separator || '
' === $separator ) {
return implode( '
', array_map( 'esc_html', $parts ) );
}
return esc_html( implode( $separator, $parts ) );
}
add_shortcode( 'a1tools_address', 'a1tools_shortcode_address' );
/**
* Shortcode: [a1tools_full_address]
* Outputs the complete address in a single line.
*
* @since 1.2.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_full_address( $atts ) {
$atts = shortcode_atts(
array(
'separator' => ', ',
'class' => '',
),
$atts,
'a1tools_full_address'
);
$vars = a1tools_get_site_variables();
if ( ! $vars ) {
return '';
}
// Use standalone full_address field if set.
if ( ! empty( $vars['full_address'] ) ) {
$output = esc_html( $vars['full_address'] );
if ( ! empty( $atts['class'] ) ) {
$output = '' . $output . '';
}
return $output;
}
// Fallback: assemble from individual address fields.
$parts = array();
// Address Line 1.
if ( ! empty( $vars['address_line1'] ) ) {
$parts[] = $vars['address_line1'];
}
// Address Line 2.
if ( ! empty( $vars['address_line2'] ) ) {
$parts[] = $vars['address_line2'];
}
// City.
if ( ! empty( $vars['city'] ) ) {
$parts[] = $vars['city'];
}
// State + ZIP (combined).
$state = ! empty( $vars['state'] ) ? $vars['state'] : '';
$zip = ! empty( $vars['zip'] ) ? $vars['zip'] : '';
if ( $state || $zip ) {
$parts[] = trim( $state . ' ' . $zip );
}
if ( empty( $parts ) ) {
return '';
}
$output = esc_html( implode( $atts['separator'], $parts ) );
if ( ! empty( $atts['class'] ) ) {
$output = '' . $output . '';
}
return $output;
}
add_shortcode( 'a1tools_full_address', 'a1tools_shortcode_full_address' );
/**
* Shortcode: [a1tools_hours]
* Outputs formatted operating hours table.
*
* @since 1.0.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_hours( $atts ) {
$atts = shortcode_atts(
array(
'format' => 'table',
'class' => 'a1tools-hours',
),
$atts,
'a1tools_hours'
);
$vars = a1tools_get_site_variables();
if ( ! $vars || empty( $vars['operating_hours'] ) ) {
return '';
}
$hours = $vars['operating_hours'];
$days = array(
'mon' => __( 'Monday', 'a1-tools' ),
'tue' => __( 'Tuesday', 'a1-tools' ),
'wed' => __( 'Wednesday', 'a1-tools' ),
'thu' => __( 'Thursday', 'a1-tools' ),
'fri' => __( 'Friday', 'a1-tools' ),
'sat' => __( 'Saturday', 'a1-tools' ),
'sun' => __( 'Sunday', 'a1-tools' ),
);
$output = '';
if ( 'list' === $atts['format'] ) {
$output = '
| ' . esc_html( $label ) . ' | ' . $time . ' |
' . esc_html__( 'No store locations found.', 'a1-tools' ) . '
'; } // Resolve API key and marker icon (shortcode attr > WP option > empty). $api_key = ! empty( $atts['api_key'] ) ? $atts['api_key'] : get_option( 'a1tools_google_maps_api_key', '' ); $marker_icon = ! empty( $atts['marker_icon'] ) ? $atts['marker_icon'] : get_option( 'a1tools_marker_icon_url', '' ); $use_js_api = ! empty( $api_key ); // Collect unique states for filter dropdown. $states = array(); foreach ( $stores as $store ) { if ( ! empty( $store['state'] ) && ! in_array( $store['state'], $states, true ) ) { $states[] = $store['state']; } } sort( $states ); // Build JSON data for JavaScript. $stores_json = array(); foreach ( $stores as $store ) { $addr_parts = array(); if ( ! empty( $store['address_line1'] ) ) { $addr_parts[] = $store['address_line1']; } if ( ! empty( $store['address_line2'] ) ) { $addr_parts[] = $store['address_line2']; } $csz = array(); if ( ! empty( $store['city'] ) ) { $csz[] = $store['city']; } if ( ! empty( $store['state'] ) ) { $csz[] = $store['state']; } if ( ! empty( $store['zip'] ) ) { $csz[] = $store['zip']; } if ( ! empty( $csz ) ) { $addr_parts[] = implode( ', ', $csz ); } $stores_json[] = array( 'id' => intval( $store['id'] ?? 0 ), 'name' => $store['name'] ?? '', 'address' => implode( ', ', $addr_parts ), 'city' => strtolower( $store['city'] ?? '' ), 'state' => strtolower( $store['state'] ?? '' ), 'zip' => $store['zip'] ?? '', 'phone' => $store['phone'] ?? '', 'email' => $store['email'] ?? '', 'website' => $store['website_url'] ?? '', 'mapsUrl' => $store['google_maps_url'] ?? '', 'image' => $store['image_url'] ?? '', 'lat' => ! empty( $store['latitude'] ) ? floatval( $store['latitude'] ) : null, 'lng' => ! empty( $store['longitude'] ) ? floatval( $store['longitude'] ) : null, ); } $unique_id = 'a1sl-' . wp_unique_id(); $map_height = intval( $atts['map_height'] ); $output = '' . esc_html( $s['address'] ) . '
'; } if ( ! empty( $s['phone'] ) ) { $phone_clean = preg_replace( '/[^0-9+]/', '', $s['phone'] ); $output .= '' . esc_html__( 'Phone:', 'a1-tools' ) . ' ' . esc_html( $s['phone'] ) . '
'; } if ( ! empty( $s['email'] ) ) { $output .= '' . esc_html__( 'Email:', 'a1-tools' ) . ' ' . esc_html( $s['email'] ) . '
'; } // Links. $dir_url = ''; if ( ! empty( $s['mapsUrl'] ) ) { $dir_url = $s['mapsUrl']; } elseif ( null !== $s['lat'] && null !== $s['lng'] ) { $dir_url = 'https://www.google.com/maps/search/?api=1&query=' . $s['lat'] . ',' . $s['lng']; } $output .= '' . esc_html( $service['description'] ) . '
'; } // Phone. if ( $show_phone && ! empty( $service['phone'] ) ) { $output .= ''; } $output .= '' . esc_html( $review['name'] ) . '
'; } // Review text. if ( ! empty( $review['text'] ) ) { $output .= '' . esc_html( $review['text'] ) . '
'; } // Source badge. if ( $show_source && ! empty( $review['source'] ) ) { $output .= '' . esc_html( $review['source'] ) . ''; } // Date. if ( $show_date && ! empty( $review['date'] ) ) { $output .= '' . esc_html( $review['date'] ) . ''; } $output .= '' . esc_html( $item['description'] ) . '
'; } $output .= '' . esc_html( $item['description'] ) . '
'; } $output .= '' . esc_html( $member['role'] ) . '
'; } // Bio. if ( $show_bio && ! empty( $member['bio'] ) ) { $output .= '' . esc_html( $member['bio'] ) . '
'; } // Contact info. if ( $show_phone && ! empty( $member['phone'] ) ) { $output .= '' . esc_html( $member['phone'] ) . '
'; } if ( $show_email && ! empty( $member['email'] ) ) { $output .= '' . esc_html( $member['email'] ) . '
'; } // Certifications. if ( $show_certs && ! empty( $member['certifications'] ) && is_array( $member['certifications'] ) ) { $output .= '
' . esc_html__( 'Cache cleared successfully!', 'a1-tools' ) . '
|
[a1tools_hours]
[a1tools_hours format="list"]
| ✓ — | ✓ — |
[a1tools_franchise]
[a1tools_franchise columns="2"]
[a1tools_franchise id="X"]
Franchise tab.', 'a1-tools' ); ?>
| ✓ — | ✓ — |
[a1tools_store_locator]
Stores tab.', 'a1-tools' ); ?>
$wpd ) { if ( isset( $saved_city_data[ $wpid ] ) ) { $city_pages[ $wpid ] = array_merge( $wpd, $saved_city_data[ $wpid ] ); } else { $city_pages[ $wpid ] = $wpd; } } ?>
[a1tools_city_phone]
[a1tools_city_phone link="yes"]
[a1tools_city_address]
Cities tab.', 'a1-tools' ); ?>
get( 'form_settings' ); $form_id = $form_settings['id'] ?? 'unknown'; $form_name = $form_settings['form_name'] ?? 'Contact Form'; // Get the raw form field values. $raw_fields = $record->get( 'fields' ); // Build a clean associative array: field_id => value. $fields = array(); foreach ( $raw_fields as $field_id => $field_data ) { $fields[ $field_id ] = $field_data['value'] ?? ''; } // Get source page URL. $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? sanitize_url( $_SERVER['HTTP_REFERER'] ) : ''; // Build the API payload. $payload = array( 'action' => 'submit', 'form_id' => $form_id, 'form_name' => $form_name, 'fields' => $fields, 'source_url' => $referrer, 'source_ip' => a1tools_get_client_ip(), 'form_secret' => $form_secret, ); // API endpoint. $api_url = 'https://tools.a-1chimney.com/api/sunday/forms.php?action=submit'; /** * Filter the A1 Tools form submission API URL. * * @since 1.5.0 * @param string $api_url The API URL. */ $api_url = apply_filters( 'a1tools_form_api_url', $api_url ); // Defer the API call to PHP shutdown so it doesn't interfere with // Elementor's form processing pipeline (MailChimp, email, etc.). // The AJAX response is sent to the browser first, then our request fires. add_action( 'shutdown', function() use ( $api_url, $payload, $form_secret ) { $response = wp_remote_post( $api_url, array( 'timeout' => 15, 'httpversion' => '1.1', 'blocking' => false, 'headers' => array( 'Content-Type' => 'application/json', 'X-Form-Secret' => $form_secret, ), 'body' => wp_json_encode( $payload ), 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { error_log( '[A1 Tools] Form submission API error: ' . $response->get_error_message() ); } }); } // Only register the form hook when form forwarding is enabled. // This prevents any interference with other Elementor form actions (e.g., MailChimp). if ( get_option( 'a1tools_form_enabled', false ) ) { add_action( 'elementor_pro/forms/new_record', 'a1tools_handle_elementor_form_submission', 99, 2 ); } /** * Get client IP address, handling proxies. * * @since 1.5.0 * @return string Client IP. */ function a1tools_get_client_ip() { if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $ips = explode( ',', sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ); return trim( $ips[0] ); } if ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) { return sanitize_text_field( $_SERVER['HTTP_X_REAL_IP'] ); } return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( $_SERVER['REMOTE_ADDR'] ) : '0.0.0.0'; }