opt('acpe_placement'); // Only add the_content injection when in auto mode if ( $placement === 'auto' && ! apply_filters('acpe_disable_auto_inject', false) ) { add_filter('the_content', [$this, 'inject_toolbar'], 12); } if ( $placement === 'auto' && ! apply_filters('acpe_disable_archive_holder', false) ) { add_action('wp_footer', [$this, 'taxonomy_toolbar_footer_holder'], 20); } add_shortcode('ac_print_email', [$this, 'shortcode']); // Archive toolbar is output in the footer in a hidden holder, then relocated by JS. //add_action('wp_footer', [$this, 'taxonomy_toolbar_footer_holder'], 20); /* ========================================================== 3) Admin settings ========================================================== */ add_action('admin_init', [$this, 'register_settings']); add_action('admin_menu', [$this, 'add_settings_page']); // Load frontend CSS on the plugin settings screen so preview looks identical. add_action('admin_enqueue_scripts', [$this, 'admin_enqueue_assets']); // Yoast: strip ACPE toolbar text from meta/OG/Twitter descriptions add_filter('wpseo_metadesc', [$this, 'yoast_strip_acpe'], 999); add_filter('wpseo_opengraph_desc', [$this, 'yoast_strip_acpe'], 999); add_filter('wpseo_twitter_description', [$this, 'yoast_strip_acpe'], 999); // Settings link on the Plugins screen add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), [$this, 'plugin_action_links'] ); } public function yoast_strip_acpe( $text ) { if ( ! is_string($text) || $text === '' ) return $text; // Remove the toolbar “word salad” Yoast sometimes picks up $bad = [ 'Print | Email ▾', 'Print | Email', 'Print', 'Email ▾', 'Email', 'Gmail', 'Yahoo Mail', 'Outlook / Hotmail', 'AOL Mail', 'Default mail app', 'Copy link', '|', '▾', ]; // Remove those tokens only when they appear as a leading block // (keeps you from accidentally deleting real content later in the text) $clean = trim($text); // If it starts with the toolbar, strip the toolbar words out if ( preg_match('/^\s*(Print\s*\|\s*Email|Print)\b/i', $clean) ) { $clean = str_replace($bad, ' ', $clean); $clean = trim(preg_replace('/\s{2,}/', ' ', $clean)); } return $clean; } /* ========================================================== SVG output sanitization - We output inline SVG markup for icons. - Use wp_kses with an allowlist so Plugin Check is satisfied. ========================================================== */ private function acpe_allowed_svg_tags(): array { return [ 'svg' => [ 'xmlns' => true, 'xmlns:xlink' => true, 'viewbox' => true, 'preserveaspectratio' => true, 'class' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'aria-hidden' => true, 'role' => true, 'focusable' => true, ], 'path' => [ 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ], 'g' => [ 'fill' => true, 'stroke' => true, 'stroke-width' => true, ], 'title' => [], ]; } /* Render an SVG icon safely (wp_kses allowlist) */ private function acpe_allowed_toolbar_html(): array { return [ 'div' => [ 'class' => true, 'id' => true, 'style' => true, 'hidden' => true, 'role' => true, 'aria-label' => true, 'data-acpe' => true, 'data-email-subject' => true, 'data-email-body' => true, 'data-acpe-url' => true, ], 'a' => [ 'href' => true, 'class' => true, 'target' => true, 'rel' => true, 'role' => true, 'aria-label' => true, 'aria-haspopup' => true, 'aria-expanded' => true, ], 'span' => [ 'class' => true, 'aria-hidden' => true, ], 'hr' => [ 'class' => true, 'aria-hidden' => true, ], // SVG tags 'svg' => [ 'xmlns' => true, 'xmlns:xlink' => true, 'viewbox' => true, 'preserveaspectRatio' => true, 'class' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'aria-hidden' => true, 'role' => true, 'focusable' => true, ], 'path' => [ 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, ], 'g' => [ 'fill' => true, 'stroke' => true, 'stroke-width' => true, ], 'title' => [], ]; } /* ========================================================== Settings defaults ========================================================== */ private function defaults() : array { return [ 'acpe_enable_posts' => 1, 'acpe_enable_pages' => 1, // Per-taxonomy toggles 'acpe_enable_category' => 0, 'acpe_enable_tag' => 0, // Position for auto-inject on singular 'acpe_position' => 'before', // before | after // Selects auto inject toolbar or to use shortcode 'acpe_placement' => 'auto', // auto | shortcode // Provider list 'acpe_providers' => ['gmail','yahoo','outlook','aol','mailto','copy'], // Icons 'acpe_icon_style' => 'minimal', // minimal | outline | solid | text (text = no icons) // Colors 'acpe_text_color' => '#000000', 'acpe_menu_bg' => '#ffffff', // NEW: display mode // both | icons | labels 'acpe_display_mode' => 'both', ]; } private function opt(string $key) { $d = $this->defaults(); return get_option($key, $d[$key] ?? null); } /* ========================================================== 1) Frontend assets ========================================================== */ public function enqueue_assets() { // Assets are small; enqueue on relevant views. // Frontend only if ( is_admin() ) return; // Only enqueue where ACPE can appear if ( ! ( is_singular() || is_home() || is_archive() || is_search() ) ) return; $url = plugin_dir_url(__FILE__); $path = plugin_dir_path(__FILE__); $css_file = $path . 'assets/ac-print-email.css'; $js_core = $path . 'assets/ac-print-email.js'; $js_a11y = $path . 'assets/ac-print-email-a11y.js'; wp_enqueue_style( 'acpe-style', $url . 'assets/ac-print-email.css', [], file_exists($css_file) ? filemtime($css_file) : '1.0' ); // Inline colors from settings (theme-agnostic) $text = sanitize_hex_color( (string) $this->opt('acpe_text_color') ); $bg = sanitize_hex_color( (string) $this->opt('acpe_menu_bg') ); if ($text || $bg) { $inline = ''; if ($text) { $inline .= ".acpe-toolbar a{ color: {$text} !important; }\n"; $inline .= ".acpe-toolbar a:hover{ opacity: .85; }\n"; } if ($bg) { $inline .= ".acpe-menu{ background: {$bg} !important; }\n"; } wp_add_inline_style('acpe-style', $inline); } wp_enqueue_script( 'acpe-core', $url . 'assets/ac-print-email.js', [], file_exists($js_core) ? filemtime($js_core) : '1.0', true ); wp_enqueue_script( 'acpe-a11y', $url . 'assets/ac-print-email-a11y.js', ['acpe-core'], file_exists($js_a11y) ? filemtime($js_a11y) : '1.0', true ); } /* ========================================================== 2) Enable logic ========================================================== */ private function enabled_on_this_view() : bool { $enabled_posts = (bool) $this->opt('acpe_enable_posts'); $enabled_pages = (bool) $this->opt('acpe_enable_pages'); if ( is_single() && $enabled_posts ) return true; if ( is_page() && $enabled_pages ) return true; return false; } private function enabled_on_this_archive() : bool { // Per-taxonomy toggles if ( is_category() && (bool) $this->opt('acpe_enable_category') ) return true; if ( is_tag() && (bool) $this->opt('acpe_enable_tag') ) return true; return false; } /* ========================================================== 3) Content injection (posts/pages) ========================================================== */ public function inject_toolbar($content) { if ( is_admin() ) return $content; if ( ! $this->enabled_on_this_view() ) return $content; // If user already placed shortcode manually, don't auto-inject. if ( has_shortcode($content, 'ac_print_email') ) return $content; // Prevent double-inject if the_content runs more than once if ( strpos($content, 'data-acpe="1"') !== false ) return $content; $position = (string) $this->opt('acpe_position'); // before|after $toolbar = $this->render_toolbar(get_the_title(), get_permalink()); return ($position === 'after') ? ($content . $toolbar) : ($toolbar . $content); } public function shortcode($atts = []) { if ( is_admin() ) return ''; // Allow shortcode anywhere (even if auto-inject disabled) return $this->render_toolbar(get_the_title(), get_permalink()); } /* ========================================================== 4) Toolbar renderer (shared) - Used for single posts/pages and archive toolbars ========================================================== */ private function render_toolbar(string $title, string $url) : string { // Prevent duplicates (template shortcode + content shortcode + archive holder, etc.) //if ( $this->acpe_rendered ) return ''; //$this->acpe_rendered = true; $providers = (array) $this->opt('acpe_providers'); $providers = array_map('sanitize_key', $providers); // NEW: display mode class $mode = sanitize_key((string) $this->opt('acpe_display_mode')); if (!in_array($mode, ['both','icons','labels'], true)) $mode = 'both'; $mode_class = 'acpe-mode-' . $mode; ob_start(); ?>
acpe_allowed_toolbar_html() ); } /* ========================================================== 5) Archive toolbar holder (category + tag) - SAFE method: output once in wp_footer hidden container - JS relocates into the archive header area ========================================================== */ public function taxonomy_toolbar_footer_holder() { if ( is_admin() ) return; if ( ! $this->enabled_on_this_archive() ) return; // Prevent duplicate archive holder output //if ( $this->acpe_rendered ) return; //$this->acpe_rendered = true; $obj = get_queried_object(); $title = ''; $url = ''; if ( is_category() ) { $title = single_cat_title('', false); $url = get_term_link($obj); } elseif ( is_tag() ) { $title = single_tag_title('', false); $url = get_term_link($obj); } else { return; } if ( is_wp_error($url) ) $url = home_url('/'); $url = esc_url_raw( $url ); echo wp_kses( '', $this->acpe_allowed_toolbar_html() ); } /* ========================================================== 6) Icons (SVG) ========================================================== */ // Load provider logo SVGs from /assets/icons when available (fallback to built-in icons) private function svg_file_icon(string $name): string { $name = sanitize_key($name); if (isset($this->svg_cache[$name])) { return $this->svg_cache[$name]; } $file = plugin_dir_path(__FILE__) . 'assets/icons/' . $name . '.svg'; if (!file_exists($file)) { $this->svg_cache[$name] = ''; return ''; } $svg = file_get_contents($file); if (!$svg) { $this->svg_cache[$name] = ''; return ''; } $svg = preg_replace('#<(script|foreignObject)\b[^>]*>(.*?)\1>#is', '', $svg); if (strpos($svg, 'class=') === false) { $svg = preg_replace('/