PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Newsletter / Sharing / ShareMetadataBuilder.php
mailpoet / lib / Newsletter / Sharing Last commit date
PublicEmailController.php 1 month ago PublicEmailRoute.php 1 month ago ShareMetadataBuilder.php 2 months ago ShareVisibility.php 2 months ago index.php 2 months ago
ShareMetadataBuilder.php
279 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Newsletter\Sharing;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\NewsletterEntity;
9 use MailPoet\Util\pQuery\pQuery;
10 use MailPoet\WP\Functions as WPFunctions;
11
12 class ShareMetadataBuilder {
13 private const COPY_BUTTON_ATTRIBUTE = 'data-mailpoet-share-copy';
14
15 /** @var WPFunctions */
16 private $wp;
17
18 public function __construct(
19 WPFunctions $wp
20 ) {
21 $this->wp = $wp;
22 }
23
24 public function injectMetadata(string $html, NewsletterEntity $newsletter, string $canonicalUrl): string {
25 $metadata = $this->buildMetadata($html, $newsletter, $canonicalUrl);
26 $position = stripos($html, '</head>');
27 if ($position === false) {
28 return $metadata . "\n" . $html;
29 }
30
31 // Inject before </head> using substr_replace rather than preg_replace so
32 // that dollar-digit sequences in the newsletter subject (e.g. "Save $50")
33 // are not interpreted as backreferences in the replacement string.
34 return substr_replace($html, $metadata . "\n", $position, 0);
35 }
36
37 /**
38 * Inject the share toolbar right after <body>. Pass $replaceStateUrl when the
39 * current URL carries subscriber-specific data (e.g. a tokenised view-in-browser
40 * link) — the toolbar's JS will rewrite the address bar to that URL on load so
41 * the subscriber's token isn't shared by accident if they copy from the bar
42 * or use their browser's share menu.
43 */
44 public function injectShareToolbar(string $html, NewsletterEntity $newsletter, string $canonicalUrl, string $replaceStateUrl = ''): string {
45 return $this->injectAfterBodyOpen($html, $this->buildShareToolbar($newsletter, $canonicalUrl, $replaceStateUrl));
46 }
47
48 public function buildMetadata(string $html, NewsletterEntity $newsletter, string $canonicalUrl): string {
49 $tags = [
50 $this->metaTag('name', 'robots', 'noindex, nofollow'),
51 ];
52
53 $title = $newsletter->getCampaignNameOrSubject();
54 if ($title === '') {
55 return implode("\n", $tags);
56 }
57
58 $description = trim($newsletter->getPreheader());
59 $image = $this->findFirstContentImage($html);
60
61 $tags[] = $this->metaTag('property', 'og:title', $title);
62 $tags[] = $this->metaTag('property', 'og:type', 'website');
63 $tags[] = $this->metaTag('property', 'og:url', $canonicalUrl);
64 $tags[] = $this->metaTag('name', 'twitter:card', $image ? 'summary_large_image' : 'summary');
65 $tags[] = $this->metaTag('name', 'twitter:title', $title);
66
67 if ($description !== '') {
68 $tags[] = $this->metaTag('property', 'og:description', $description);
69 $tags[] = $this->metaTag('name', 'twitter:description', $description);
70 }
71
72 if ($image) {
73 $tags[] = $this->metaTag('property', 'og:image', $image['src']);
74 $tags[] = $this->metaTag('name', 'twitter:image', $image['src']);
75 if ($image['alt'] !== '') {
76 $tags[] = $this->metaTag('property', 'og:image:alt', $image['alt']);
77 }
78 }
79
80 return implode("\n", $tags);
81 }
82
83 private function buildShareToolbar(NewsletterEntity $newsletter, string $canonicalUrl, string $replaceStateUrl): string {
84 $title = __('Share this email', 'mailpoet');
85 $description = __('Copy the public link or share it on your favorite channel.', 'mailpoet');
86 $emailTitle = $newsletter->getCampaignNameOrSubject() ?: __('Email', 'mailpoet');
87 $copyLabel = __('Copy link', 'mailpoet');
88 $shareLabel = _x('Share', 'Web Share button label', 'mailpoet');
89 $encodedUrl = rawurlencode($canonicalUrl);
90 $encodedTitle = rawurlencode($emailTitle);
91
92 $hostAttrs = ' data-mailpoet-share-host';
93 if ($replaceStateUrl !== '') {
94 $hostAttrs .= ' data-mailpoet-share-replace-state="' . $this->wp->escAttr($replaceStateUrl) . '"';
95 }
96
97 $toolbar = $this->buildShareToolbarStyles()
98 . '<div class="mailpoet-share-toolbar" role="region" aria-label="' . $this->wp->escAttr($title) . '">'
99 . '<div class="mailpoet-share-toolbar__inner">'
100 . '<div class="mailpoet-share-toolbar__copy">'
101 . '<strong>' . $this->wp->escHtml($title) . '</strong>'
102 . '<span>' . $this->wp->escHtml($description) . '</span>'
103 . '</div>'
104 . '<div class="mailpoet-share-toolbar__controls">'
105 . '<div class="mailpoet-share-toolbar__url-row">'
106 . '<input class="mailpoet-share-toolbar__url components-text-control__input" type="url" readonly aria-label="' . $this->wp->escAttr(__('Public share link', 'mailpoet')) . '" value="' . $this->wp->escAttr($canonicalUrl) . '" />'
107 . $this->buildShareToolbarCopyButton($canonicalUrl, $copyLabel)
108 . '</div>'
109 . '<div class="mailpoet-share-toolbar__actions">'
110 . '<button type="button" class="mailpoet-share-toolbar__button mailpoet-share-toolbar__web-share components-button is-secondary" aria-label="' . $this->wp->escAttr($shareLabel) . '" data-mailpoet-web-share="' . $this->wp->escAttr($canonicalUrl) . '" data-mailpoet-web-share-title="' . $this->wp->escAttr($emailTitle) . '">'
111 . $this->buildShareToolbarActionContent('share', $shareLabel)
112 . '</button>'
113 . $this->buildShareToolbarLink('facebook', $this->wp->escUrl('https://www.facebook.com/sharer/sharer.php?u=' . $encodedUrl), 'Facebook', true)
114 . $this->buildShareToolbarLink('x', $this->wp->escUrl('https://twitter.com/intent/tweet?url=' . $encodedUrl . '&text=' . $encodedTitle), 'X', true)
115 . $this->buildShareToolbarLink('whatsapp', $this->wp->escUrl('https://api.whatsapp.com/send?text=' . rawurlencode(trim($emailTitle . ' ' . $canonicalUrl))), 'WhatsApp', true)
116 . $this->buildShareToolbarLink('email', $this->wp->escUrl('mailto:?subject=' . $encodedTitle . '&body=' . $encodedUrl), __('Email', 'mailpoet'), true)
117 . '</div></div></div></div>';
118
119 return '<div' . $hostAttrs . '>' . $toolbar . '</div>' . $this->buildShareToolbarScript();
120 }
121
122 private function buildShareToolbarCopyButton(string $canonicalUrl, string $copyLabel): string {
123 return '<button type="button" class="mailpoet-share-toolbar__button mailpoet-share-toolbar__copy-button components-button is-secondary" aria-label="' . $this->wp->escAttr($copyLabel) . '" ' . self::COPY_BUTTON_ATTRIBUTE . '="' . $this->wp->escAttr($canonicalUrl) . '" data-mailpoet-share-copied-label="' . $this->wp->escAttr(__('Copied', 'mailpoet')) . '">'
124 . $this->buildShareToolbarActionContent('copy', $copyLabel)
125 . '</button>';
126 }
127
128 private function buildShareToolbarLink(string $iconName, string $url, string $label, bool $opensNewTab): string {
129 $isSocialLink = in_array($iconName, ['facebook', 'x', 'whatsapp', 'email'], true);
130 $className = $isSocialLink
131 ? 'mailpoet-share-toolbar__link mailpoet-share-toolbar__social-button mailpoet-share-toolbar__social-button--' . $this->wp->escAttr($iconName) . ' components-button'
132 : 'mailpoet-share-toolbar__link components-button is-secondary';
133
134 return '<a class="' . $className . '" href="' . $url . '" aria-label="' . $this->wp->escAttr($label) . '"'
135 . ($opensNewTab ? ' target="_blank" rel="noopener noreferrer"' : '')
136 . '>'
137 . $this->buildShareToolbarActionContent($iconName, $label)
138 . '</a>';
139 }
140
141 private function buildShareToolbarActionContent(string $iconName, string $label): string {
142 return $this->buildShareToolbarIcon($iconName)
143 . '<span data-mailpoet-share-label aria-live="polite">' . $this->wp->escHtml($label) . '</span>';
144 }
145
146 private function buildShareToolbarIcon(string $iconName): string {
147 $icons = [
148 'copy' => '<path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1Zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2Zm0 16H8V7h11v14Z" />',
149 'share' => '<path d="M18 16.1c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11A2.99 2.99 0 1 0 15 5c0 .24.04.47.09.7L8.04 9.81A3 3 0 1 0 8.04 14.2l7.12 4.18c-.05.2-.07.41-.07.62a2.91 2.91 0 1 0 2.91-2.9Z" />',
150 'facebook' => '<path d="M14 8.5V6.75c0-.5.4-.75.85-.75H17V2.5h-3.1C10.83 2.5 9 4.33 9 7.2v1.3H6v3.75h3V22h5v-9.75h3.25l.55-3.75H14Z" />',
151 'x' => '<path d="M13.7 10.6 21.4 2h-1.8l-6.7 7.5L7.6 2H1.5l8.1 11.5L1.5 22h1.8l7.1-7.8 5.7 7.8h6.1l-8.5-11.4Zm-2.5 2.8-.8-1.1L3.9 3.4h2.8l5.2 7.2.8 1.1 6.9 9h-2.8l-5.6-7.3Z" />',
152 'whatsapp' => '<path d="M12.04 2C6.58 2 2.15 6.43 2.15 11.9c0 1.75.46 3.45 1.33 4.95L2 22l5.29-1.39a9.85 9.85 0 0 0 4.75 1.21h.01c5.46 0 9.9-4.43 9.9-9.89A9.9 9.9 0 0 0 12.04 2Zm5.82 14.2c-.25.7-1.45 1.34-2.02 1.43-.52.08-1.18.12-1.9-.12-.44-.14-1-.33-1.72-.64-3.02-1.3-4.99-4.33-5.14-4.53-.15-.2-1.23-1.64-1.23-3.13 0-1.49.78-2.22 1.06-2.52.28-.3.61-.37.82-.37h.59c.19 0 .44-.07.69.53.25.6.85 2.08.92 2.23.08.15.13.33.03.53-.1.2-.15.33-.3.51-.15.18-.32.4-.45.54-.15.15-.31.31-.13.61.18.3.8 1.31 1.71 2.12 1.18 1.05 2.17 1.37 2.47 1.52.3.15.48.13.66-.08.18-.2.76-.89.97-1.2.2-.3.41-.25.69-.15.28.1 1.78.84 2.08.99.3.15.51.23.58.36.08.13.08.75-.17 1.45Z" />',
153 'email' => '<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2Zm0 4-8 5-8-5V6l8 5 8-5v2Z" />',
154 ];
155 if (!isset($icons[$iconName])) {
156 return '';
157 }
158
159 return '<span class="mailpoet-share-toolbar__icon mailpoet-share-toolbar__icon--' . $this->wp->escAttr($iconName) . '" aria-hidden="true">'
160 . '<svg viewBox="0 0 24 24" focusable="false">' . $icons[$iconName] . '</svg>'
161 . '</span>';
162 }
163
164 private function buildShareToolbarStyles(): string {
165 return '<style>'
166 . '.mailpoet-share-toolbar{background:#fff;border-bottom:1px solid #dcdcde;color:#1d2327;font-family:system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;margin-bottom:20px;position:relative;width:100%}'
167 . '.mailpoet-share-toolbar *{box-sizing:border-box}'
168 . '.mailpoet-share-toolbar__inner{align-items:center;display:flex;gap:24px;margin:0 auto;max-width:800px;padding:16px 24px;width:100%}'
169 . '.mailpoet-share-toolbar__copy{display:flex;flex:0 0 260px;flex-direction:column;gap:4px;min-width:0}'
170 . '.mailpoet-share-toolbar__copy strong{font-size:16px;font-weight:600;line-height:1.3}'
171 . '.mailpoet-share-toolbar__copy span{color:#50575e;font-size:13px;line-height:1.4;text-wrap:balance}'
172 . '.mailpoet-share-toolbar__controls{display:flex;flex:1 1 auto;flex-direction:column;gap:10px;min-width:0}'
173 . '.mailpoet-share-toolbar__url-row{align-items:flex-end;display:flex;gap:10px}'
174 . '.mailpoet-share-toolbar__url{border:1px solid #8c8f94;border-radius:2px;color:#1d2327;flex:1 1 auto;font-family:monospace;font-size:13px;line-height:20px;min-height:40px;min-width:0;padding:8px}'
175 . '.mailpoet-share-toolbar__actions{display:flex;flex-wrap:wrap;gap:10px}'
176 . '.mailpoet-share-toolbar__button,.mailpoet-share-toolbar__link{align-items:center;border-radius:2px;display:inline-flex;font-size:13px;font-weight:500;gap:6px;justify-content:center;line-height:20px;min-height:40px;padding:6px 12px;text-decoration:none;white-space:nowrap}'
177 . '.mailpoet-share-toolbar__button.components-button.is-secondary,.mailpoet-share-toolbar__link.components-button.is-secondary{background:#fff;border:1px solid #2271b1;color:#2271b1;cursor:pointer}'
178 . '.mailpoet-share-toolbar__button.components-button.is-secondary:hover,.mailpoet-share-toolbar__button.components-button.is-secondary:focus,.mailpoet-share-toolbar__link.components-button.is-secondary:hover,.mailpoet-share-toolbar__link.components-button.is-secondary:focus{border-color:#135e96;color:#135e96}'
179 . '.mailpoet-share-toolbar__copy-button{flex:0 0 auto}'
180 . '.mailpoet-share-toolbar__social-button.components-button{border:1px solid transparent;box-shadow:none;color:#fff;flex:1 1 0}'
181 . '.mailpoet-share-toolbar__social-button.components-button:hover,.mailpoet-share-toolbar__social-button.components-button:focus{border-color:transparent;color:#fff;opacity:.85}'
182 . '.mailpoet-share-toolbar__social-button--facebook.components-button{background:#1877f2}'
183 . '.mailpoet-share-toolbar__social-button--x.components-button{background:#111}'
184 . '.mailpoet-share-toolbar__social-button--whatsapp.components-button{background:#25d366}'
185 . '.mailpoet-share-toolbar__social-button--email.components-button{background:#3858e9}'
186 . '.mailpoet-share-toolbar__icon{display:inline-flex;height:20px;width:20px}'
187 . '.mailpoet-share-toolbar__icon svg{display:block;fill:currentColor;height:20px;width:20px}'
188 . '.mailpoet-share-toolbar__web-share{display:none}'
189 . '@media(max-width:782px){.mailpoet-share-toolbar__inner{align-items:stretch;flex-direction:column;gap:12px;padding:16px}.mailpoet-share-toolbar__copy{flex:auto}.mailpoet-share-toolbar__url-row{align-items:stretch;flex-direction:column}.mailpoet-share-toolbar__button,.mailpoet-share-toolbar__link{width:100%}}'
190 . '</style>';
191 }
192
193 private function buildShareToolbarScript(): string {
194 return '<script>'
195 . '(function(){'
196 . 'var hosts=document.querySelectorAll("[data-mailpoet-share-host]");'
197 // Replace state once if any host asks us to scrub a tokenised URL from the address bar.
198 . 'for(var r=0;r<hosts.length;r++){var replace=hosts[r].getAttribute("data-mailpoet-share-replace-state");if(replace&&window.history&&typeof window.history.replaceState==="function"){try{window.history.replaceState(null,document.title,replace);}catch(error){}break;}}'
199 . 'function copyUrl(url){'
200 . 'if(navigator.clipboard&&window.isSecureContext){return navigator.clipboard.writeText(url);}'
201 . 'var input=document.createElement("textarea");'
202 . 'input.value=url;input.setAttribute("readonly","");input.style.position="fixed";input.style.left="-9999px";document.body.appendChild(input);input.select();'
203 . 'try{document.execCommand("copy");}catch(error){}'
204 . 'document.body.removeChild(input);return Promise.resolve();'
205 . '}'
206 . 'var copyButtons=document.querySelectorAll("[' . self::COPY_BUTTON_ATTRIBUTE . ']");'
207 . 'for(var i=0;i<copyButtons.length;i++){if(copyButtons[i].dataset.mailpoetShareBound){continue;}copyButtons[i].dataset.mailpoetShareBound="1";copyButtons[i].addEventListener("click",function(){var button=this;var labelElement=button.querySelector("[data-mailpoet-share-label]");var label=labelElement?labelElement.textContent:button.textContent;copyUrl(button.getAttribute("' . self::COPY_BUTTON_ATTRIBUTE . '")).then(function(){var copiedLabel=button.getAttribute("data-mailpoet-share-copied-label")||label;if(labelElement){labelElement.textContent=copiedLabel;}else{button.textContent=copiedLabel;}window.setTimeout(function(){if(labelElement){labelElement.textContent=label;}else{button.textContent=label;}},2000);});});}'
208 . 'var shareButtons=document.querySelectorAll("[data-mailpoet-web-share]");'
209 . 'for(var j=0;j<shareButtons.length;j++){if(!navigator.share||shareButtons[j].dataset.mailpoetShareBound){continue;}shareButtons[j].dataset.mailpoetShareBound="1";shareButtons[j].style.display="inline-flex";shareButtons[j].addEventListener("click",function(){navigator.share({title:this.getAttribute("data-mailpoet-web-share-title"),url:this.getAttribute("data-mailpoet-web-share")}).catch(function(){});});}'
210 // Promote each host into its own shadow root so email styles can\'t leak in or out.
211 // Handlers were bound above on the original nodes; moving them into the shadow root preserves listeners.
212 . 'for(var k=0;k<hosts.length;k++){var host=hosts[k];if(host.shadowRoot||typeof host.attachShadow!=="function"){continue;}try{var shadow=host.attachShadow({mode:"open"});while(host.firstChild){shadow.appendChild(host.firstChild);}}catch(error){}}'
213 . '})();'
214 . '</script>';
215 }
216
217 private function injectAfterBodyOpen(string $html, string $markup): string {
218 $bodyPosition = stripos($html, '<body');
219 if ($bodyPosition === false) {
220 return $markup . $html;
221 }
222
223 $bodyOpeningTagEnd = strpos($html, '>', $bodyPosition);
224 if ($bodyOpeningTagEnd === false) {
225 return $markup . $html;
226 }
227
228 return substr($html, 0, $bodyOpeningTagEnd + 1) . $markup . substr($html, $bodyOpeningTagEnd + 1);
229 }
230
231 /**
232 * @return array{src: string, alt: string}|null
233 */
234 private function findFirstContentImage(string $html): ?array {
235 $dom = pQuery::parseStr($html);
236 foreach ($dom->query('img') as $image) {
237 $src = trim((string)$image->src);
238 if (!$this->isUsableImage($src, $image)) {
239 continue;
240 }
241 return [
242 'src' => $src,
243 'alt' => trim((string)$image->alt),
244 ];
245 }
246 return null;
247 }
248
249 private function isUsableImage(string $src, $image): bool {
250 if ($src === '' || !preg_match('/^https?:\/\//i', $src)) {
251 return false;
252 }
253
254 $haystack = strtolower($src . ' ' . (string)$image->alt);
255 foreach (['fake-logo', 'your-logo-placeholder', 'social-icons', 'mailpoet-logo', 'powered-by-mailpoet'] as $needle) {
256 if (strpos($haystack, $needle) !== false) {
257 return false;
258 }
259 }
260
261 $width = isset($image->width) ? (int)$image->width : 0;
262 $height = isset($image->height) ? (int)$image->height : 0;
263 if (($width > 0 && $width < 64) || ($height > 0 && $height < 64)) {
264 return false;
265 }
266
267 return true;
268 }
269
270 private function metaTag(string $attribute, string $name, string $content): string {
271 return sprintf(
272 '<meta %s="%s" content="%s" />',
273 $attribute,
274 $this->wp->escAttr($name),
275 $this->wp->escAttr($content)
276 );
277 }
278 }
279