'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 ); } // ============================================================================ // 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 = ''; } else { $output = ''; foreach ( $days as $key => $label ) { $time = isset( $hours[ $key ] ) && ! empty( $hours[ $key ] ) ? esc_html( $hours[ $key ] ) : esc_html__( 'Closed', 'a1-tools' ); $output .= ''; } $output .= '
' . esc_html( $label ) . '' . $time . '
'; } return $output; } add_shortcode( 'a1tools_hours', 'a1tools_shortcode_hours' ); /** * Get SVG markup for icons not available in Font Awesome 5. * * @since 1.5.0 * @param string $icon_key The SVG icon key (e.g. 'svg-x'). * @return string SVG markup or empty string. */ function a1tools_get_svg_icon( $icon_key ) { $svgs = array( 'svg-x' => '', ); return isset( $svgs[ $icon_key ] ) ? $svgs[ $icon_key ] : ''; } /** * Get icon HTML (SVG or Font Awesome tag). * * @since 1.5.0 * @param string $icon Icon class or SVG key. * @return string HTML markup for the icon. */ function a1tools_get_icon_html( $icon ) { $svg = a1tools_get_svg_icon( $icon ); if ( $svg ) { return $svg; } return ''; } /** * Shortcode: [a1tools_social_links] * Outputs social media icons/links with optional styling. * * @since 1.0.0 * @since 1.2.0 Added styling parameters. * @param array $atts Shortcode attributes. * @return string Shortcode output. */ function a1tools_shortcode_social_links( $atts ) { $atts = shortcode_atts( array( 'class' => 'a1tools-social-links', 'icon_class' => '', 'style' => '', // default, outline, minimal, light, dark 'shape' => 'rounded', // rounded, circle, square 'size' => '40', // button size in px 'icon_size' => '18', // icon size in px 'spacing' => '8', // spacing between icons in px 'alignment' => 'left', // left, center, right 'color' => '', // custom color (hex) 'hover' => 'scale', // scale, lift, rotate, pulse, none ), $atts, 'a1tools_social_links' ); $vars = a1tools_get_site_variables(); if ( ! $vars ) { return ''; } $platforms = array( 'facebook_url' => array( 'label' => 'Facebook', 'icon' => 'fab fa-facebook-f', 'color' => '#1877F2' ), 'instagram_url' => array( 'label' => 'Instagram', 'icon' => 'fab fa-instagram', 'color' => '#E4405F' ), 'youtube_url' => array( 'label' => 'YouTube', 'icon' => 'fab fa-youtube', 'color' => '#FF0000' ), 'twitter_url' => array( 'label' => 'X (Twitter)', 'icon' => 'svg-x', 'color' => '#000000' ), 'linkedin_url' => array( 'label' => 'LinkedIn', 'icon' => 'fab fa-linkedin-in', 'color' => '#0A66C2' ), 'tiktok_url' => array( 'label' => 'TikTok', 'icon' => 'fab fa-tiktok', 'color' => '#000000' ), 'yelp_url' => array( 'label' => 'Yelp', 'icon' => 'fab fa-yelp', 'color' => '#D32323' ), 'google_business_url' => array( 'label' => 'Google', 'icon' => 'fab fa-google', 'color' => '#4285F4' ), 'pinterest_url' => array( 'label' => 'Pinterest', 'icon' => 'fab fa-pinterest-p', 'color' => '#E60023' ), 'bbb_url' => array( 'label' => 'BBB', 'icon' => 'fas fa-shield-alt', 'color' => '#006CB7' ), 'nextdoor_url' => array( 'label' => 'Nextdoor', 'icon' => 'fas fa-home', 'color' => '#8ED500' ), 'houzz_url' => array( 'label' => 'Houzz', 'icon' => 'fab fa-houzz', 'color' => '#4DBC15' ), 'angi_url' => array( 'label' => 'Angi', 'icon' => 'fas fa-tools', 'color' => '#FF6153' ), 'thumbtack_url' => array( 'label' => 'Thumbtack', 'icon' => 'fas fa-thumbtack', 'color' => '#009FD9' ), 'homeadvisor_url' => array( 'label' => 'HomeAdvisor', 'icon' => 'fas fa-home', 'color' => '#EF6C00' ), ); // If no style is set, use simple output (backwards compatible). if ( empty( $atts['style'] ) ) { $output = '
'; foreach ( $platforms as $key => $data ) { if ( ! empty( $vars[ $key ] ) ) { $icon = ! empty( $atts['icon_class'] ) ? $atts['icon_class'] : $data['icon']; $output .= ''; $output .= a1tools_get_icon_html( $icon ); $output .= '' . esc_html( $data['label'] ) . ''; $output .= ''; } } $output .= '
'; return $output; } // Styled output. $style = $atts['style']; $shape = $atts['shape']; $size = (int) $atts['size']; $icon_size = (int) $atts['icon_size']; $spacing = (int) $atts['spacing']; $alignment = $atts['alignment']; $custom_color = $atts['color']; $hover_effect = $atts['hover']; // Generate unique ID. static $instance_count = 0; $instance_count++; $widget_id = 'a1tools-social-sc-' . $instance_count; // Border radius based on shape. $border_radius = '8px'; if ( 'circle' === $shape ) { $border_radius = '50%'; } elseif ( 'square' === $shape ) { $border_radius = '0'; } // Build CSS for this instance. $css = '#' . esc_attr( $widget_id ) . ' { text-align:' . esc_attr( $alignment ) . '; }'; $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { '; $css .= 'width:' . (int) $size . 'px;height:' . (int) $size . 'px;'; $css .= 'margin:0 ' . (int) ( $spacing / 2 ) . 'px ' . (int) $spacing . 'px;'; $css .= 'font-size:' . (int) $icon_size . 'px;'; $css .= 'display:inline-flex;align-items:center;justify-content:center;'; $css .= 'text-decoration:none;transition:all 0.3s ease;'; $css .= 'border-radius:' . esc_attr( $border_radius ) . ';'; $css .= '}'; // Style-specific CSS. if ( 'default' === $style || 'official' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { color: #fff; }'; } elseif ( 'outline' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: transparent; border: 2px solid; }'; } elseif ( 'minimal' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: transparent; }'; } elseif ( 'light' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: #f5f5f5; }'; } elseif ( 'dark' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: #333; color: #fff; }'; } // Hover effects. if ( 'scale' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: scale(1.1); }'; } elseif ( 'lift' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); }'; } elseif ( 'rotate' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: rotate(15deg); }'; } elseif ( 'pulse' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { animation: a1tools-pulse 0.5s ease; }'; $css .= '@keyframes a1tools-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } }'; } // Ensure the style handle exists before adding inline CSS. if ( ! wp_style_is( 'a1tools-social', 'registered' ) ) { wp_register_style( 'a1tools-social', false, array(), A1TOOLS_VERSION ); } if ( ! wp_style_is( 'a1tools-social', 'enqueued' ) ) { wp_enqueue_style( 'a1tools-social' ); } wp_add_inline_style( 'a1tools-social', $css ); $output = '
'; foreach ( $platforms as $key => $data ) { if ( ! empty( $vars[ $key ] ) ) { $item_color = $custom_color ? $custom_color : $data['color']; $inline_style = ''; if ( 'default' === $style || 'official' === $style ) { $inline_style = 'background-color:' . esc_attr( $item_color ) . ';'; } elseif ( 'outline' === $style ) { $inline_style = 'border-color:' . esc_attr( $item_color ) . ';color:' . esc_attr( $item_color ) . ';'; } elseif ( 'minimal' === $style || 'light' === $style ) { $inline_style = 'color:' . esc_attr( $item_color ) . ';'; } $icon = ! empty( $atts['icon_class'] ) ? $atts['icon_class'] : $data['icon']; $output .= ''; $output .= a1tools_get_icon_html( $icon ); $output .= '' . esc_html( $data['label'] ) . ''; $output .= ''; } } $output .= '
'; return $output; } add_shortcode( 'a1tools_social_links', 'a1tools_shortcode_social_links' ); /** * Shortcode: [a1tools_city_name] * Outputs the city name (for geo-targeting). * * @since 1.0.8 * @param array $atts Shortcode attributes. * @return string Shortcode output. */ function a1tools_shortcode_city_name( $atts ) { $atts = shortcode_atts( array( 'default' => '', 'class' => '', ), $atts, 'a1tools_city_name' ); $value = a1tools_get_variable( 'city_name', $atts['default'] ); if ( empty( $value ) ) { return esc_html( $atts['default'] ); } if ( ! empty( $atts['class'] ) ) { return '' . esc_html( $value ) . ''; } return esc_html( $value ); } add_shortcode( 'a1tools_city_name', 'a1tools_shortcode_city_name' ); /** * Shortcode: [a1tools_state] * Outputs the state/location name (for geo-targeting). * * @since 1.0.8 * @param array $atts Shortcode attributes. * @return string Shortcode output. */ function a1tools_shortcode_state( $atts ) { $atts = shortcode_atts( array( 'default' => '', 'class' => '', ), $atts, 'a1tools_state' ); $value = a1tools_get_variable( 'location_name', $atts['default'] ); if ( empty( $value ) ) { return esc_html( $atts['default'] ); } if ( ! empty( $atts['class'] ) ) { return '' . esc_html( $value ) . ''; } return esc_html( $value ); } add_shortcode( 'a1tools_state', 'a1tools_shortcode_state' ); /** * Shortcode: [a1tools_google_map] * Outputs a Google Maps embed or link. * * @since 1.0.8 * @param array $atts Shortcode attributes. * @return string Shortcode output. */ function a1tools_shortcode_google_map( $atts ) { $atts = shortcode_atts( array( 'type' => 'link', 'width' => '100%', 'height' => '400', 'class' => 'a1tools-google-map', 'text' => __( 'View on Google Maps', 'a1-tools' ), 'target' => '_blank', ), $atts, 'a1tools_google_map' ); $url = a1tools_get_variable( 'google_maps_url', '' ); if ( empty( $url ) ) { return ''; } // If type is 'embed', create an iframe if ( 'embed' === $atts['type'] ) { // Check if URL is already an embed URL or convert it $embed_url = $url; // If it's a regular Google Maps URL, try to convert to embed format if ( false !== strpos( $url, 'google.com/maps' ) && false === strpos( $url, '/embed' ) ) { // Extract place or coordinates and create embed URL if ( preg_match( '/place\/([^\/]+)/', $url, $matches ) ) { $place = $matches[1]; $embed_url = 'https://www.google.com/maps/embed/v1/place?key=&q=' . urlencode( $place ); } elseif ( preg_match( '/@(-?\d+\.\d+),(-?\d+\.\d+)/', $url, $matches ) ) { $lat = $matches[1]; $lng = $matches[2]; $embed_url = 'https://maps.google.com/maps?q=' . $lat . ',' . $lng . '&output=embed'; } } return ''; } // Default: return a link $rel = '_blank' === $atts['target'] ? ' rel="noopener noreferrer"' : ''; return '' . esc_html( $atts['text'] ) . ''; } add_shortcode( 'a1tools_google_map', 'a1tools_shortcode_google_map' ); // ============================================================================ // REST API ENDPOINTS // ============================================================================ /** * Register REST API routes. * * @since 1.0.0 * @return void */ function a1tools_register_rest_routes() { // Get site variables (public). register_rest_route( 'a1-tools/v1', '/site-variables', array( 'methods' => 'GET', 'callback' => 'a1tools_rest_get_variables', 'permission_callback' => '__return_true', ) ); // Clear cache (requires admin). register_rest_route( 'a1-tools/v1', '/site-variables/clear-cache', array( 'methods' => 'POST', 'callback' => 'a1tools_rest_clear_cache', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); // Yoast meta endpoints. register_rest_route( 'a1-tools/v1', '/yoast-meta/(?P\d+)', array( 'methods' => 'POST', 'callback' => 'a1tools_update_yoast_meta', 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, 'args' => array( 'id' => array( 'description' => 'Post ID', 'type' => 'integer', 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ) && $param > 0; }, ), ), ) ); register_rest_route( 'a1-tools/v1', '/yoast-meta/(?P\d+)', array( 'methods' => 'GET', 'callback' => 'a1tools_get_yoast_meta', 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, 'args' => array( 'id' => array( 'description' => 'Post ID', 'type' => 'integer', 'required' => true, 'validate_callback' => function ( $param ) { return is_numeric( $param ) && $param > 0; }, ), ), ) ); } add_action( 'rest_api_init', 'a1tools_register_rest_routes' ); /** * REST: Get site variables. * * @since 1.0.0 * @return WP_REST_Response|WP_Error Response object. */ function a1tools_rest_get_variables() { $variables = a1tools_get_site_variables(); if ( null === $variables ) { return new WP_Error( 'fetch_error', __( 'Could not fetch site variables', 'a1-tools' ), array( 'status' => 500 ) ); } return new WP_REST_Response( array( 'success' => true, 'variables' => $variables, 'cached' => false !== get_transient( A1TOOLS_CACHE_KEY ), ), 200 ); } /** * REST: Clear variables cache. * * @since 1.0.0 * @return WP_REST_Response Response object. */ function a1tools_rest_clear_cache() { a1tools_clear_cache(); return new WP_REST_Response( array( 'success' => true, 'message' => __( 'Cache cleared', 'a1-tools' ), ), 200 ); } // ============================================================================ // YOAST SEO INTEGRATION // ============================================================================ /** * Get Yoast meta field mapping. * * @since 1.0.0 * @return array Mapping of API keys to Yoast meta keys. */ function a1tools_get_yoast_meta_mapping() { return array( 'title' => '_yoast_wpseo_title', 'metadesc' => '_yoast_wpseo_metadesc', 'focuskw' => '_yoast_wpseo_focuskw', 'focuskeywords' => '_yoast_wpseo_focuskeywords', 'keywordsynonyms' => '_yoast_wpseo_keywordsynonyms', 'meta-robots-noindex' => '_yoast_wpseo_meta-robots-noindex', 'meta-robots-nofollow' => '_yoast_wpseo_meta-robots-nofollow', 'meta-robots-adv' => '_yoast_wpseo_meta-robots-adv', 'canonical' => '_yoast_wpseo_canonical', 'redirect' => '_yoast_wpseo_redirect', 'is_cornerstone' => '_yoast_wpseo_is_cornerstone', 'opengraph-title' => '_yoast_wpseo_opengraph-title', 'opengraph-description' => '_yoast_wpseo_opengraph-description', 'opengraph-image' => '_yoast_wpseo_opengraph-image', 'opengraph-image-id' => '_yoast_wpseo_opengraph-image-id', 'twitter-title' => '_yoast_wpseo_twitter-title', 'twitter-description' => '_yoast_wpseo_twitter-description', 'twitter-image' => '_yoast_wpseo_twitter-image', 'twitter-image-id' => '_yoast_wpseo_twitter-image-id', 'schema_page_type' => '_yoast_wpseo_schema_page_type', 'schema_article_type' => '_yoast_wpseo_schema_article_type', 'linkdex' => '_yoast_wpseo_linkdex', 'content_score' => '_yoast_wpseo_content_score', 'primary_category' => '_yoast_wpseo_primary_category', ); } /** * Sanitize Yoast meta value based on field type. * * @since 1.0.0 * @param string $key Field key. * @param mixed $value Value to sanitize. * @return mixed Sanitized value. */ function a1tools_sanitize_yoast_value( $key, $value ) { $json_fields = array( 'focuskeywords', 'keywordsynonyms' ); if ( in_array( $key, $json_fields, true ) ) { if ( is_array( $value ) ) { return wp_json_encode( $value ); } if ( is_string( $value ) ) { $decoded = json_decode( $value ); if ( JSON_ERROR_NONE === json_last_error() ) { return $value; } return wp_json_encode( array( $value ) ); } return ''; } $bool_fields = array( 'is_cornerstone', 'meta-robots-noindex', 'meta-robots-nofollow' ); if ( in_array( $key, $bool_fields, true ) ) { return $value ? '1' : '0'; } $int_fields = array( 'linkdex', 'content_score', 'primary_category', 'opengraph-image-id', 'twitter-image-id' ); if ( in_array( $key, $int_fields, true ) ) { return (int) $value; } $url_fields = array( 'canonical', 'redirect', 'opengraph-image', 'twitter-image' ); if ( in_array( $key, $url_fields, true ) ) { return esc_url_raw( $value ); } $text_fields = array( 'title', 'metadesc', 'opengraph-title', 'opengraph-description', 'twitter-title', 'twitter-description' ); if ( in_array( $key, $text_fields, true ) ) { return sanitize_text_field( $value ); } if ( is_string( $value ) ) { return sanitize_text_field( $value ); } return $value; } /** * Update Yoast SEO meta fields for a post. * * @since 1.0.0 * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. */ function a1tools_update_yoast_meta( $request ) { $post_id = (int) $request['id']; $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'post_not_found', __( 'Post not found', 'a1-tools' ), array( 'status' => 404 ) ); } if ( ! current_user_can( 'edit_post', $post_id ) ) { return new WP_Error( 'forbidden', __( 'You do not have permission to edit this post', 'a1-tools' ), array( 'status' => 403 ) ); } $body = $request->get_json_params(); if ( empty( $body ) ) { return new WP_Error( 'no_data', __( 'No data provided', 'a1-tools' ), array( 'status' => 400 ) ); } $mapping = a1tools_get_yoast_meta_mapping(); $updated = array(); $errors = array(); foreach ( $body as $key => $value ) { if ( isset( $mapping[ $key ] ) ) { $meta_key = $mapping[ $key ]; } elseif ( 0 === strpos( $key, '_yoast_wpseo_' ) ) { $meta_key = $key; } else { continue; } $sanitized_value = a1tools_sanitize_yoast_value( $key, $value ); $result = update_post_meta( $post_id, $meta_key, $sanitized_value ); if ( false !== $result ) { $updated[ $key ] = $sanitized_value; } else { $current_value = get_post_meta( $post_id, $meta_key, true ); if ( $current_value === $sanitized_value || $current_value === (string) $sanitized_value ) { $updated[ $key ] = $sanitized_value; } else { $errors[ $key ] = 'Failed to update'; } } } if ( class_exists( 'WPSEO_Meta' ) ) { // Trigger Yoast SEO's native action hook to refresh their internal cache. // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is Yoast's hook, not ours. do_action( 'wpseo_saved_postdata' ); } return new WP_REST_Response( array( 'success' => true, 'post_id' => $post_id, 'updated' => $updated, 'errors' => $errors, ), 200 ); } /** * Get Yoast SEO meta fields for a post. * * @since 1.0.0 * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. */ function a1tools_get_yoast_meta( $request ) { $post_id = (int) $request['id']; $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'post_not_found', __( 'Post not found', 'a1-tools' ), array( 'status' => 404 ) ); } $mapping = a1tools_get_yoast_meta_mapping(); $meta = array(); foreach ( $mapping as $api_key => $meta_key ) { $value = get_post_meta( $post_id, $meta_key, true ); if ( '' !== $value && false !== $value ) { $meta[ $api_key ] = $value; } } return new WP_REST_Response( array( 'success' => true, 'post_id' => $post_id, 'meta' => $meta, ), 200 ); } // ============================================================================ // ELEMENTOR INTEGRATION // ============================================================================ /** * Register Elementor dynamic tags. * * @since 1.0.0 * @param \Elementor\Core\DynamicTags\Manager $dynamic_tags_manager Dynamic tags manager. * @return void */ function a1tools_register_elementor_tags( $dynamic_tags_manager ) { // Register dynamic tag group. $dynamic_tags_manager->register_group( 'a1-tools', array( 'title' => __( 'A1 Tools', 'a1-tools' ), ) ); // Include and register the dynamic tag class. require_once A1TOOLS_PLUGIN_DIR . 'includes/class-a1-tools-elementor-tag.php'; if ( class_exists( 'A1Tools_Elementor_Tag' ) ) { $dynamic_tags_manager->register( new A1Tools_Elementor_Tag() ); } } add_action( 'elementor/dynamic_tags/register', 'a1tools_register_elementor_tags' ); /** * Register Elementor widgets. * * @since 1.2.0 * @param \Elementor\Widgets_Manager $widgets_manager Widgets manager. * @return void */ function a1tools_register_elementor_widgets( $widgets_manager ) { // Include and register the Social Icons widget. require_once A1TOOLS_PLUGIN_DIR . 'includes/class-a1-tools-elementor-widget.php'; if ( class_exists( 'A1Tools_Social_Icons_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_Social_Icons_Elementor_Widget() ); } // Include and register the Hours widget. require_once A1TOOLS_PLUGIN_DIR . 'includes/class-a1-tools-hours-widget.php'; if ( class_exists( 'A1Tools_Hours_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_Hours_Elementor_Widget() ); } // Include and register info-box widgets (Phone, Email, Location). require_once A1TOOLS_PLUGIN_DIR . 'includes/class-a1-tools-info-box-widget.php'; if ( class_exists( 'A1Tools_Phone_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_Phone_Elementor_Widget() ); } if ( class_exists( 'A1Tools_Email_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_Email_Elementor_Widget() ); } if ( class_exists( 'A1Tools_Location_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_Location_Elementor_Widget() ); } // Include and register combined Contact Info widget. require_once A1TOOLS_PLUGIN_DIR . 'includes/class-a1-tools-contact-info-widget.php'; if ( class_exists( 'A1Tools_Contact_Info_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_Contact_Info_Elementor_Widget() ); } // Include and register CTA button widget. require_once A1TOOLS_PLUGIN_DIR . 'includes/class-a1-tools-cta-widget.php'; if ( class_exists( 'A1Tools_CTA_Elementor_Widget' ) ) { $widgets_manager->register( new A1Tools_CTA_Elementor_Widget() ); } } add_action( 'elementor/widgets/register', 'a1tools_register_elementor_widgets' ); /** * Register Elementor widget styles. * * Registers the base CSS for the A1 Social Icons Elementor widget as an inline * stylesheet so it loads once and works in both frontend and editor. * * @since 1.4.8 * @return void */ function a1tools_register_elementor_styles() { $css = ' .a1tools-social-icons-styled { display: flex; flex-wrap: wrap; align-items: center; } .a1tools-social-icons-styled .a1tools-social-icon { display: inline-flex; align-items: center; justify-content: center; text-decoration: none !important; transition: all 0.3s ease; box-sizing: border-box; } .a1tools-social-icons-styled .a1tools-social-icon i { color: inherit; line-height: 1; } .a1tools-social-icons-styled .a1tools-social-icon .a1tools-svg-icon { width: 1em; height: 1em; fill: currentColor; } /* Style presets for non-custom styles */ .a1tools-style-default .a1tools-social-icon, .a1tools-style-official .a1tools-social-icon { color: #fff; } .a1tools-style-default .a1tools-social-icon i, .a1tools-style-official .a1tools-social-icon i { color: #fff; } .a1tools-style-outline .a1tools-social-icon { background: transparent; border: 2px solid; } .a1tools-style-minimal .a1tools-social-icon { background: transparent; } .a1tools-style-light .a1tools-social-icon { background: #f5f5f5; } .a1tools-style-dark .a1tools-social-icon { background: #333; color: #fff; } .a1tools-style-dark .a1tools-social-icon i { color: #fff; } /* Hover effects via prefix_class */ .a1tools-hover-scale .a1tools-social-icon:hover { transform: scale(1.1); } .a1tools-hover-lift .a1tools-social-icon:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); } .a1tools-hover-rotate .a1tools-social-icon:hover { transform: rotate(15deg); } .a1tools-hover-pulse .a1tools-social-icon:hover { animation: a1tools-pulse 0.5s ease; } @keyframes a1tools-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } } '; wp_register_style( 'a1tools-elementor-icons', false, array(), A1TOOLS_VERSION ); wp_enqueue_style( 'a1tools-elementor-icons' ); wp_add_inline_style( 'a1tools-elementor-icons', $css ); } add_action( 'wp_enqueue_scripts', 'a1tools_register_elementor_styles' ); add_action( 'elementor/editor/before_enqueue_styles', 'a1tools_register_elementor_styles' ); add_action( 'elementor/preview/enqueue_styles', 'a1tools_register_elementor_styles' ); /** * Register Hours Elementor widget styles. * * @since 1.5.2 * @return void */ function a1tools_register_hours_styles() { $css = ' .a1tools-hours-widget { box-sizing: border-box; } .a1tools-hours-title { display: flex; align-items: center; } .a1tools-hours-widget[style*="text-align: center"] .a1tools-hours-title, .a1tools-hours-widget[style*="text-align:center"] .a1tools-hours-title { justify-content: center; } .a1tools-hours-widget[style*="text-align: right"] .a1tools-hours-title, .a1tools-hours-widget[style*="text-align:right"] .a1tools-hours-title { justify-content: flex-end; } .a1tools-hours-title-text { font-size: inherit; font-weight: bold; } .a1tools-hours-icon { display: inline-flex; align-items: center; justify-content: center; line-height: 1; } .a1tools-hours-icon i { color: inherit; margin: 0; padding: 0; line-height: 1; width: 1em; height: 1em; display: inline-flex; align-items: center; justify-content: center; } .a1tools-hours-icon svg { fill: currentColor; margin: 0; padding: 0; } .a1tools-hours-row { display: flex; align-items: baseline; gap: 16px; } .a1tools-hours-has-divider .a1tools-hours-row { border-bottom: 1px solid #e0e0e0; } .a1tools-hours-has-divider .a1tools-hours-row:last-child { border-bottom: none; } .a1tools-hours-day { font-weight: 600; flex-shrink: 0; } /* Fixed-width day column so times always align */ .a1tools-hours-layout-list .a1tools-hours-day, .a1tools-hours-layout-table .a1tools-hours-day { min-width: 110px; } /* Inline layout: horizontal flow */ .a1tools-hours-layout-inline .a1tools-hours-list { display: flex; flex-wrap: wrap; gap: 8px 20px; } .a1tools-hours-layout-inline .a1tools-hours-row { gap: 6px; } '; wp_register_style( 'a1tools-elementor-hours', false, array(), A1TOOLS_VERSION ); wp_enqueue_style( 'a1tools-elementor-hours' ); wp_add_inline_style( 'a1tools-elementor-hours', $css ); } add_action( 'wp_enqueue_scripts', 'a1tools_register_hours_styles' ); add_action( 'elementor/editor/before_enqueue_styles', 'a1tools_register_hours_styles' ); add_action( 'elementor/preview/enqueue_styles', 'a1tools_register_hours_styles' ); /** * Register Info Box / Contact Info Elementor widget styles. * * Shared CSS for Phone, Email, Location, and combined Contact Info widgets. * * @since 1.5.3 * @return void */ function a1tools_register_info_box_styles() { $css = ' /* ── Info Box (Phone, Email, Location) ─────────────────── */ .a1tools-info-box { display: flex; align-items: flex-start; box-sizing: border-box; transition: border-color 0.2s ease; } .a1tools-info-box-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; line-height: 1; } .a1tools-info-box-icon i { color: inherit; margin: 0; padding: 0; line-height: 1; width: 1em; height: 1em; display: inline-flex; align-items: center; justify-content: center; } .a1tools-info-box-icon svg { fill: currentColor; margin: 0; padding: 0; } .a1tools-info-box-content { display: flex; flex-direction: column; min-width: 0; } .a1tools-info-box-title { font-weight: 600; line-height: 1.3; } .a1tools-info-box-value { text-decoration: none; color: inherit; word-break: break-word; } /* Link wrapper – make behave like the
it replaces */ a.a1tools-info-box-link, a.a1tools-info-box-link:visited { text-decoration: none; color: inherit; display: flex; align-items: flex-start; } a.a1tools-info-box-link .a1tools-info-box-icon, a.a1tools-info-box-link .a1tools-info-box-title, a.a1tools-info-box-link .a1tools-info-box-value { transition: color 0.2s ease; } a.a1tools-info-box-link .a1tools-info-box-icon svg { transition: fill 0.2s ease; } /* Hover underline on value only */ a.a1tools-underline-hover:hover .a1tools-info-box-value { text-decoration: underline; } .a1tools-info-box-empty { font-style: italic; opacity: 0.6; } /* ── Contact Info (combined widget) ────────────────────── */ .a1tools-contact-info-widget { display: flex; flex-direction: column; transition: border-color 0.2s ease; } .a1tools-contact-info-item { display: flex; align-items: flex-start; } .a1tools-has-dividers > .a1tools-contact-info-item + .a1tools-contact-info-item { border-top: 1px solid #e0e0e0; padding-top: 20px; } '; wp_register_style( 'a1tools-elementor-info-box', false, array(), A1TOOLS_VERSION ); wp_enqueue_style( 'a1tools-elementor-info-box' ); wp_add_inline_style( 'a1tools-elementor-info-box', $css ); } add_action( 'wp_enqueue_scripts', 'a1tools_register_info_box_styles' ); add_action( 'elementor/editor/before_enqueue_styles', 'a1tools_register_info_box_styles' ); add_action( 'elementor/preview/enqueue_styles', 'a1tools_register_info_box_styles' ); /** * Register CTA button widget styles. * * @since 1.5.9 * @return void */ function a1tools_register_cta_styles() { $css = ' /* ── CTA Button ───────────────────────────────────────── */ .a1tools-cta-wrapper { display: flex; } .a1tools-cta-button { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; cursor: pointer; box-sizing: border-box; transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; line-height: 1; color: inherit; } .a1tools-cta-button:visited { color: inherit; } .a1tools-cta-icon { display: inline-flex; align-items: center; justify-content: center; line-height: 1; } .a1tools-cta-icon i { color: inherit; } .a1tools-cta-icon svg { fill: currentColor; } /* ── CTA Animations (continuous) ──────────────────────── */ .a1tools-anim--pulse { animation: a1ctaPulse 1.5s ease infinite; } .a1tools-anim--heartbeat { animation: a1ctaHeartbeat 1.5s ease infinite; } .a1tools-anim--bounce { animation: a1ctaBounce 1.5s ease infinite; } .a1tools-anim--shake { animation: a1ctaShake 1.5s ease infinite; } .a1tools-anim--glow { animation: a1ctaGlow 1.5s ease infinite; } .a1tools-anim--wiggle { animation: a1ctaWiggle 1.5s ease infinite; } .a1tools-anim--flash { animation: a1ctaFlash 1.5s ease infinite; } .a1tools-anim--swing { animation: a1ctaSwing 1.5s ease infinite; transform-origin: top center; } .a1tools-anim--rubberband { animation: a1ctaRubberBand 1.5s ease infinite; } .a1tools-anim--tada { animation: a1ctaTada 1.5s ease infinite; } /* ── CTA Animations (hover-only) ─────────────────────── */ .a1tools-anim-hover { animation: none; } .a1tools-anim-hover--pulse:hover { animation: a1ctaPulse 1.5s ease infinite; } .a1tools-anim-hover--heartbeat:hover { animation: a1ctaHeartbeat 1.5s ease infinite; } .a1tools-anim-hover--bounce:hover { animation: a1ctaBounce 1.5s ease infinite; } .a1tools-anim-hover--shake:hover { animation: a1ctaShake 1.5s ease infinite; } .a1tools-anim-hover--glow:hover { animation: a1ctaGlow 1.5s ease infinite; } .a1tools-anim-hover--wiggle:hover { animation: a1ctaWiggle 1.5s ease infinite; } .a1tools-anim-hover--flash:hover { animation: a1ctaFlash 1.5s ease infinite; } .a1tools-anim-hover--swing:hover { animation: a1ctaSwing 1.5s ease infinite; transform-origin: top center; } .a1tools-anim-hover--rubberband:hover { animation: a1ctaRubberBand 1.5s ease infinite; } .a1tools-anim-hover--tada:hover { animation: a1ctaTada 1.5s ease infinite; } /* 1. Pulse — gentle scale up/down */ @keyframes a1ctaPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } } /* 2. Heartbeat — double-beat like a heart */ @keyframes a1ctaHeartbeat { 0% { transform: scale(1); } 14% { transform: scale(1.1); } 28% { transform: scale(1); } 42% { transform: scale(1.1); } 70% { transform: scale(1); } 100% { transform: scale(1); } } /* 3. Bounce — hops up and down */ @keyframes a1ctaBounce { 0%, 100% { transform: translateY(0); } 30% { transform: translateY(-8px); } 50% { transform: translateY(0); } 65% { transform: translateY(-4px); } 80% { transform: translateY(0); } } /* 4. Shake — side-to-side */ @keyframes a1ctaShake { 0%, 100% { transform: translateX(0); } 10% { transform: translateX(-5px); } 20% { transform: translateX(5px); } 30% { transform: translateX(-5px); } 40% { transform: translateX(5px); } 50% { transform: translateX(0); } } /* 5. Glow — pulsing box-shadow (uses --a1-glow-color CSS var) */ .a1tools-cta-button { --a1-glow-color: #ffffff; } @keyframes a1ctaGlow { 0%, 100% { box-shadow: 0 0 5px var(--a1-glow-color); opacity: 0.9; } 50% { box-shadow: 0 0 20px var(--a1-glow-color), 0 0 40px var(--a1-glow-color); opacity: 1; } } /* 6. Wiggle — rotates back and forth */ @keyframes a1ctaWiggle { 0%, 100% { transform: rotate(0deg); } 15% { transform: rotate(-4deg); } 30% { transform: rotate(4deg); } 45% { transform: rotate(-4deg); } 60% { transform: rotate(4deg); } 75% { transform: rotate(0deg); } } /* 7. Flash — opacity blink */ @keyframes a1ctaFlash { 0%, 50%, 100% { opacity: 1; } 25%, 75% { opacity: 0.4; } } /* 8. Swing — pendulum from top */ @keyframes a1ctaSwing { 0%, 100% { transform: rotate(0deg); } 20% { transform: rotate(8deg); } 40% { transform: rotate(-6deg); } 60% { transform: rotate(4deg); } 80% { transform: rotate(-2deg); } } /* 9. Rubber Band — stretch and snap */ @keyframes a1ctaRubberBand { 0%, 100% { transform: scaleX(1) scaleY(1); } 30% { transform: scaleX(1.15) scaleY(0.9); } 40% { transform: scaleX(0.9) scaleY(1.1); } 50% { transform: scaleX(1.05) scaleY(0.95); } 65% { transform: scaleX(0.98) scaleY(1.02); } 75% { transform: scaleX(1); } } /* 10. Tada — scale + rotation for attention */ @keyframes a1ctaTada { 0%, 100% { transform: scale(1) rotate(0deg); } 10% { transform: scale(0.95) rotate(-3deg); } 20% { transform: scale(0.95) rotate(-3deg); } 30% { transform: scale(1.08) rotate(3deg); } 40% { transform: scale(1.08) rotate(-3deg); } 50% { transform: scale(1.08) rotate(3deg); } 60% { transform: scale(1.08) rotate(-3deg); } 70% { transform: scale(1.08) rotate(3deg); } 80% { transform: scale(1) rotate(0deg); } } '; wp_register_style( 'a1tools-elementor-cta', false, array(), A1TOOLS_VERSION ); wp_enqueue_style( 'a1tools-elementor-cta' ); wp_add_inline_style( 'a1tools-elementor-cta', $css ); } add_action( 'wp_enqueue_scripts', 'a1tools_register_cta_styles' ); add_action( 'elementor/editor/before_enqueue_styles', 'a1tools_register_cta_styles' ); add_action( 'elementor/preview/enqueue_styles', 'a1tools_register_cta_styles' ); // ============================================================================ // ADMIN NOTICES // ============================================================================ /** * Display admin notice if site variables cannot be fetched. * * @since 1.0.0 * @return void */ function a1tools_admin_notices() { // Only show on plugins page. $screen = get_current_screen(); if ( ! $screen || 'plugins' !== $screen->id ) { return; } $variables = a1tools_get_site_variables(); if ( null === $variables ) { ?>

'integer', 'default' => A1TOOLS_DEFAULT_CACHE_EXPIRY, 'sanitize_callback' => 'absint', ) ); // Form submission integration settings. register_setting( 'a1tools_settings', 'a1tools_form_enabled', array( 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', ) ); register_setting( 'a1tools_settings', 'a1tools_form_secret', array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ) ); register_setting( 'a1tools_settings', 'a1tools_form_board_id', array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ) ); register_setting( 'a1tools_settings', 'a1tools_form_group_id', array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ) ); } add_action( 'admin_init', 'a1tools_register_settings' ); /** * Enqueue admin styles and scripts. * * @since 1.3.2 * @param string $hook The current admin page. * @return void */ function a1tools_admin_enqueue_scripts( $hook ) { // Only load on our settings page. if ( 'toplevel_page_a1-tools' !== $hook ) { return; } // Register and enqueue admin styles. wp_register_style( 'a1tools-admin', false, array(), A1TOOLS_VERSION ); wp_enqueue_style( 'a1tools-admin' ); $admin_css = ' .a1tools-shortcode-cell { display: flex; align-items: center; gap: 8px; } .a1tools-shortcode-cell code { font-size: 11px; flex: 1; } .a1tools-copy-btn { background: none; border: 1px solid #ddd; border-radius: 4px; padding: 4px 8px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: #666; transition: all 0.2s ease; } .a1tools-copy-btn:hover { background: #f0f0f0; border-color: #999; color: #333; } .a1tools-copy-btn.copied { background: #46b450; border-color: #46b450; color: #fff; } .a1tools-copy-btn .dashicons { font-size: 14px; width: 14px; height: 14px; } '; wp_add_inline_style( 'a1tools-admin', $admin_css ); // Register and enqueue admin scripts. wp_register_script( 'a1tools-admin', false, array(), A1TOOLS_VERSION, true ); wp_enqueue_script( 'a1tools-admin' ); $admin_js = ' function a1toolsGenerateSecret() { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var secret = ""; for (var i = 0; i < 48; i++) { secret += chars.charAt(Math.floor(Math.random() * chars.length)); } document.getElementById("a1tools_form_secret").value = secret; } function a1toolsCopyShortcode(btn, shortcode) { navigator.clipboard.writeText(shortcode).then(function() { btn.classList.add("copied"); btn.innerHTML = \' Copied!\'; setTimeout(function() { btn.classList.remove("copied"); btn.innerHTML = \' Copy\'; }, 2000); }).catch(function(err) { var textarea = document.createElement("textarea"); textarea.value = shortcode; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); btn.classList.add("copied"); btn.innerHTML = \' Copied!\'; setTimeout(function() { btn.classList.remove("copied"); btn.innerHTML = \' Copy\'; }, 2000); }); } '; wp_add_inline_script( 'a1tools-admin', $admin_js ); } add_action( 'admin_enqueue_scripts', 'a1tools_admin_enqueue_scripts' ); /** * Render settings page. * * @since 1.0.9 * @return void */ function a1tools_settings_page() { // Handle cache clear action. if ( isset( $_POST['a1tools_clear_cache'] ) && check_admin_referer( 'a1tools_clear_cache_nonce' ) ) { a1tools_clear_cache(); echo '

' . esc_html__( 'Cache cleared successfully!', 'a1-tools' ) . '

'; } $cache_expiry = (int) get_option( 'a1tools_cache_expiry', A1TOOLS_DEFAULT_CACHE_EXPIRY ); $cached_data = get_transient( A1TOOLS_CACHE_KEY ); $is_cached = false !== $cached_data; ?>




array( 'business_name' => array( 'label' => 'Business Name', 'shortcode' => '[a1tools_var key="business_name"]' ), 'city_name' => array( 'label' => 'City Name', 'shortcode' => '[a1tools_city_name]' ), 'location_name' => array( 'label' => 'State', 'shortcode' => '[a1tools_state]' ), 'tagline' => array( 'label' => 'Tagline', 'shortcode' => '[a1tools_var key="tagline"]' ), 'google_maps_url' => array( 'label' => 'Google Maps URL', 'shortcode' => '[a1tools_google_map]' ), ), 'Contact Information' => array( 'phone_primary' => array( 'label' => 'Primary Phone', 'shortcode' => '[a1tools_var key="phone_primary"]' ), 'phone_secondary' => array( 'label' => 'Secondary Phone', 'shortcode' => '[a1tools_var key="phone_secondary"]' ), 'email_primary' => array( 'label' => 'Primary Email', 'shortcode' => '[a1tools_var key="email_primary"]' ), 'email_secondary' => array( 'label' => 'Secondary Email', 'shortcode' => '[a1tools_var key="email_secondary"]' ), ), 'Address' => array( 'address_line1' => array( 'label' => 'Address Line 1', 'shortcode' => '[a1tools_var key="address_line1"]' ), 'address_line2' => array( 'label' => 'Address Line 2', 'shortcode' => '[a1tools_var key="address_line2"]' ), 'city' => array( 'label' => 'City', 'shortcode' => '[a1tools_var key="city"]' ), 'state' => array( 'label' => 'State', 'shortcode' => '[a1tools_var key="state"]' ), 'zip' => array( 'label' => 'ZIP Code', 'shortcode' => '[a1tools_var key="zip"]' ), 'country' => array( 'label' => 'Country', 'shortcode' => '[a1tools_var key="country"]' ), 'address_formatted' => array( 'label' => 'Full Address (multi-line)', 'shortcode' => '[a1tools_address]' ), 'full_address' => array( 'label' => 'Full Address (single line)', 'shortcode' => '[a1tools_full_address]' ), ), 'Social Media' => array( 'facebook_url' => array( 'label' => 'Facebook', 'shortcode' => '[a1tools_var key="facebook_url"]' ), 'instagram_url' => array( 'label' => 'Instagram', 'shortcode' => '[a1tools_var key="instagram_url"]' ), 'youtube_url' => array( 'label' => 'YouTube', 'shortcode' => '[a1tools_var key="youtube_url"]' ), 'twitter_url' => array( 'label' => 'Twitter/X', 'shortcode' => '[a1tools_var key="twitter_url"]' ), 'linkedin_url' => array( 'label' => 'LinkedIn', 'shortcode' => '[a1tools_var key="linkedin_url"]' ), 'tiktok_url' => array( 'label' => 'TikTok', 'shortcode' => '[a1tools_var key="tiktok_url"]' ), 'yelp_url' => array( 'label' => 'Yelp', 'shortcode' => '[a1tools_var key="yelp_url"]' ), 'google_business_url' => array( 'label' => 'Google Business', 'shortcode' => '[a1tools_var key="google_business_url"]' ), 'pinterest_url' => array( 'label' => 'Pinterest', 'shortcode' => '[a1tools_var key="pinterest_url"]' ), 'bbb_url' => array( 'label' => 'BBB', 'shortcode' => '[a1tools_var key="bbb_url"]' ), 'nextdoor_url' => array( 'label' => 'Nextdoor', 'shortcode' => '[a1tools_var key="nextdoor_url"]' ), 'houzz_url' => array( 'label' => 'Houzz', 'shortcode' => '[a1tools_var key="houzz_url"]' ), 'angi_url' => array( 'label' => 'Angi', 'shortcode' => '[a1tools_var key="angi_url"]' ), 'thumbtack_url' => array( 'label' => 'Thumbtack', 'shortcode' => '[a1tools_var key="thumbtack_url"]' ), 'homeadvisor_url' => array( 'label' => 'HomeAdvisor', 'shortcode' => '[a1tools_var key="homeadvisor_url"]' ), ), ); ?>

$fields ) : ?>

$info ) : ?>

'Monday', 'tue' => 'Tuesday', 'wed' => 'Wednesday', 'thu' => 'Thursday', 'fri' => 'Friday', 'sat' => 'Saturday', 'sun' => 'Sunday', ); foreach ( $days as $key => $label ) : $hours = isset( $variables['operating_hours'][ $key ] ) ? $variables['operating_hours'][ $key ] : ''; ?>

[a1tools_hours] [a1tools_hours format="list"]

__( 'Display social media icons from A1 Tools with customizable styling.', 'a1-tools' ), 'classname' => 'a1tools-social-widget', ) ); } /** * Frontend display. * * @param array $args Widget arguments. * @param array $instance Widget instance. */ public function widget( $args, $instance ) { $vars = a1tools_get_site_variables(); if ( ! $vars ) { return; } // Widget settings with defaults. $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; $shape = ! empty( $instance['shape'] ) ? $instance['shape'] : 'rounded'; $style = ! empty( $instance['style'] ) ? $instance['style'] : 'default'; $size = ! empty( $instance['size'] ) ? (int) $instance['size'] : 40; $icon_size = ! empty( $instance['icon_size'] ) ? (int) $instance['icon_size'] : 18; $spacing = ! empty( $instance['spacing'] ) ? (int) $instance['spacing'] : 8; $alignment = ! empty( $instance['alignment'] ) ? $instance['alignment'] : 'left'; $custom_color = ! empty( $instance['custom_color'] ) ? $instance['custom_color'] : ''; $hover_effect = ! empty( $instance['hover_effect'] ) ? $instance['hover_effect'] : 'scale'; // Platform configurations with official colors. $platforms = array( 'facebook_url' => array( 'label' => 'Facebook', 'icon' => 'fab fa-facebook-f', 'color' => '#1877F2' ), 'instagram_url' => array( 'label' => 'Instagram', 'icon' => 'fab fa-instagram', 'color' => '#E4405F' ), 'youtube_url' => array( 'label' => 'YouTube', 'icon' => 'fab fa-youtube', 'color' => '#FF0000' ), 'twitter_url' => array( 'label' => 'X (Twitter)', 'icon' => 'svg-x', 'color' => '#000000' ), 'linkedin_url' => array( 'label' => 'LinkedIn', 'icon' => 'fab fa-linkedin-in', 'color' => '#0A66C2' ), 'tiktok_url' => array( 'label' => 'TikTok', 'icon' => 'fab fa-tiktok', 'color' => '#000000' ), 'yelp_url' => array( 'label' => 'Yelp', 'icon' => 'fab fa-yelp', 'color' => '#D32323' ), 'google_business_url' => array( 'label' => 'Google', 'icon' => 'fab fa-google', 'color' => '#4285F4' ), 'pinterest_url' => array( 'label' => 'Pinterest', 'icon' => 'fab fa-pinterest-p', 'color' => '#E60023' ), 'bbb_url' => array( 'label' => 'BBB', 'icon' => 'fas fa-shield-alt', 'color' => '#006CB7' ), 'nextdoor_url' => array( 'label' => 'Nextdoor', 'icon' => 'fas fa-home', 'color' => '#8ED500' ), 'houzz_url' => array( 'label' => 'Houzz', 'icon' => 'fab fa-houzz', 'color' => '#4DBC15' ), 'angi_url' => array( 'label' => 'Angi', 'icon' => 'fas fa-tools', 'color' => '#FF6153' ), 'thumbtack_url' => array( 'label' => 'Thumbtack', 'icon' => 'fas fa-thumbtack', 'color' => '#009FD9' ), 'homeadvisor_url' => array( 'label' => 'HomeAdvisor', 'icon' => 'fas fa-home', 'color' => '#EF6C00' ), ); // Check if any platforms have URLs. $has_platforms = false; foreach ( $platforms as $key => $data ) { if ( ! empty( $vars[ $key ] ) ) { $has_platforms = true; break; } } if ( ! $has_platforms ) { return; } // Generate unique ID for this widget instance. $widget_id = 'a1tools-social-' . $this->number; // Build inline styles. $container_styles = array( 'text-align' => $alignment, ); $link_styles = array( 'width' => $size . 'px', 'height' => $size . 'px', 'margin' => '0 ' . ( $spacing / 2 ) . 'px ' . $spacing . 'px', 'font-size' => $icon_size . 'px', 'display' => 'inline-flex', 'align-items' => 'center', 'justify-content' => 'center', 'text-decoration' => 'none', 'transition' => 'all 0.3s ease', ); // Shape styles. switch ( $shape ) { case 'circle': $link_styles['border-radius'] = '50%'; break; case 'square': $link_styles['border-radius'] = '0'; break; case 'rounded': default: $link_styles['border-radius'] = '8px'; break; } // Output widget. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget args are pre-escaped by WordPress. echo $args['before_widget']; if ( $title ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget args are pre-escaped by WordPress. echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; } // Build CSS for this widget instance. $css = '#' . esc_attr( $widget_id ) . ' { '; foreach ( $container_styles as $prop => $val ) { $css .= esc_attr( $prop ) . ':' . esc_attr( $val ) . ';'; } $css .= '}'; $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { '; foreach ( $link_styles as $prop => $val ) { $css .= esc_attr( $prop ) . ':' . esc_attr( $val ) . ';'; } $css .= '}'; // Style-specific CSS. if ( 'default' === $style || 'official' === $style ) { // Official colors or custom color background. $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { color: #fff; }'; } elseif ( 'outline' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: transparent; border: 2px solid; }'; } elseif ( 'minimal' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: transparent; }'; } elseif ( 'light' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: #f5f5f5; }'; } elseif ( 'dark' === $style ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: #333; color: #fff; }'; } // Hover effects. if ( 'scale' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: scale(1.1); }'; } elseif ( 'lift' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); }'; } elseif ( 'rotate' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: rotate(15deg); }'; } elseif ( 'pulse' === $hover_effect ) { $css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { animation: a1tools-pulse 0.5s ease; }'; $css .= '@keyframes a1tools-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } }'; } // Add CSS using WordPress enqueue system. wp_register_style( 'a1tools-social-widget-' . $this->number, false, array(), A1TOOLS_VERSION ); wp_enqueue_style( 'a1tools-social-widget-' . $this->number ); wp_add_inline_style( 'a1tools-social-widget-' . $this->number, $css ); // Output icons container. echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget args are pre-escaped by WordPress. echo $args['after_widget']; } /** * Backend form. * * @param array $instance Widget instance. */ public function form( $instance ) { $title = ! empty( $instance['title'] ) ? $instance['title'] : ''; $shape = ! empty( $instance['shape'] ) ? $instance['shape'] : 'rounded'; $style = ! empty( $instance['style'] ) ? $instance['style'] : 'default'; $size = ! empty( $instance['size'] ) ? (int) $instance['size'] : 40; $icon_size = ! empty( $instance['icon_size'] ) ? (int) $instance['icon_size'] : 18; $spacing = ! empty( $instance['spacing'] ) ? (int) $instance['spacing'] : 8; $alignment = ! empty( $instance['alignment'] ) ? $instance['alignment'] : 'left'; $custom_color = ! empty( $instance['custom_color'] ) ? $instance['custom_color'] : ''; $hover_effect = ! empty( $instance['hover_effect'] ) ? $instance['hover_effect'] : 'scale'; ?>

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'; }