PluginProbe
YayMail – WooCommerce Email Customizer / 4.4.5
YayMail – WooCommerce Email Customizer v4.4.5
4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 trunk 1.9.6 2.1.4 2.1.5 3.2.2 3.2.6 3.2.7.1 3.2.8.1 3.2.9 3.3 3.3.1 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4 3.4.1 All 57 releases
yaymail / src / SocialIcons / SocialIconEndpoint.php

SocialIconEndpoint.php in YayMail – WooCommerce Email Customizer 4.4.5, at src/SocialIcons/SocialIconEndpoint.php

190 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace YayMail\SocialIcons;
4
5 use YayMail\Utils\SingletonTrait;
6
7 /**
8 * Serves dynamically tinted social icon PNGs for the Custom theme.
9 *
10 * Query params:
11 * - action: yaymail_social_icon
12 * - icon: social slug (whitelist)
13 * - color: hex without # (3 or 6 chars)
14 * - size: output size in px (optional)
15 */
16 class SocialIconEndpoint {
17
18 use SingletonTrait;
19
20 private const ACTION = 'yaymail_social_icon';
21
22 private const ICONS = [
23 'behance',
24 'discord',
25 'dribble',
26 'facebook',
27 'github',
28 'google',
29 'instagram',
30 'linkedin',
31 'medium',
32 'messenger',
33 'pinterest',
34 'reddit',
35 'skype',
36 'snapchat',
37 'spotify',
38 'telegram',
39 'tiktok',
40 'twitch',
41 'twitter',
42 'viber',
43 'vimeo',
44 'website',
45 'wechat',
46 'whatsapp',
47 'youtube',
48 'zillow',
49 ];
50
51 public function __construct() {
52 $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
53
54 if ( self::ACTION !== $action ) {
55 return;
56 }
57
58 $this->serve();
59 }
60
61 /**
62 * Build a public URL for a tinted social icon.
63 *
64 * @param string $icon Social slug.
65 * @param string $color Hex color with or without #.
66 * @param int $size Output size in px.
67 */
68 public static function get_url( $icon, $color, $size = 64 ) {
69 $color = strtolower( ltrim( (string) $color, '#' ) );
70 // Extra safety: strip any non-hex leftovers if a bad value slips through.
71 $color = preg_replace( '/[^0-9a-f]/', '', $color );
72 if ( 3 !== strlen( $color ) && 6 !== strlen( $color ) ) {
73 $color = '333333';
74 }
75 $size = max( 16, min( 256, absint( $size ) ) );
76
77 return add_query_arg(
78 [
79 'action' => self::ACTION,
80 'icon' => sanitize_key( $icon ),
81 'color' => $color,
82 'size' => $size,
83 ],
84 home_url( '/' )
85 );
86 }
87
88 private function serve() {
89 if ( ! function_exists( 'imagecreatefrompng' ) || ! function_exists( 'imagepng' ) ) {
90 status_header( 503 );
91 exit;
92 }
93
94 $icon = isset( $_GET['icon'] ) ? sanitize_key( wp_unslash( $_GET['icon'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
95 $color = isset( $_GET['color'] ) ? sanitize_text_field( wp_unslash( $_GET['color'] ) ) : '333333'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
96 $size = isset( $_GET['size'] ) ? absint( wp_unslash( $_GET['size'] ) ) : 64; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
97
98 $color = ltrim( $color, '#' );
99 if ( ! in_array( $icon, self::ICONS, true ) || ! preg_match( '/^[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/', $color ) ) {
100 status_header( 404 );
101 exit;
102 }
103
104 if ( 3 === strlen( $color ) ) {
105 $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
106 }
107 $color = strtolower( $color );
108 $size = max( 16, min( 256, $size ) );
109
110 $mask_path = YAYMAIL_PLUGIN_PATH . 'assets/images/social-icons/' . $icon . '/custom.png';
111 if ( ! file_exists( $mask_path ) ) {
112 status_header( 404 );
113 exit;
114 }
115
116 $cache_key = md5( $icon . '-' . $color . '-' . $size . '-' . (string) filemtime( $mask_path ) );
117 $etag = '"' . $cache_key . '"';
118 if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) && trim( wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) === $etag ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
119 status_header( 304 );
120 exit;
121 }
122
123 $this->send_image_headers( $etag );
124
125 // Disk cache: (icon, color, size, mask-mtime) always renders identical
126 // bytes, so once generated once on this server, skip GD entirely and
127 // stream the cached file straight from disk.
128 $cache_path = SocialIconImageCache::get_path( $cache_key );
129 if ( $cache_path && file_exists( $cache_path ) ) {
130 readfile( $cache_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_put_contents
131 exit;
132 }
133
134 $rgb = [
135 hexdec( substr( $color, 0, 2 ) ),
136 hexdec( substr( $color, 2, 2 ) ),
137 hexdec( substr( $color, 4, 2 ) ),
138 ];
139
140 $mask = imagecreatefrompng( $mask_path );
141 if ( false === $mask ) {
142 status_header( 500 );
143 exit;
144 }
145
146 imagealphablending( $mask, false );
147 imagesavealpha( $mask, true );
148
149 $src_w = imagesx( $mask );
150 $src_h = imagesy( $mask );
151
152 // Source masks are solid white icons whose shape lives entirely in the
153 // alpha channel, so a single native colorize call recolors every opaque
154 // pixel to the target color (255 + offset = target) while leaving the
155 // alpha untouched — equivalent to, but far faster than, a per-pixel loop.
156 imagefilter( $mask, IMG_FILTER_COLORIZE, $rgb[0] - 255, $rgb[1] - 255, $rgb[2] - 255 );
157
158 $out = imagecreatetruecolor( $size, $size );
159 imagealphablending( $out, false );
160 imagesavealpha( $out, true );
161 $transparent = imagecolorallocatealpha( $out, 0, 0, 0, 127 );
162 imagefilledrectangle( $out, 0, 0, $size, $size, $transparent );
163
164 imagecopyresampled( $out, $mask, 0, 0, 0, 0, $size, $size, $src_w, $src_h );
165
166 imagedestroy( $mask );
167
168 // Capture the PNG bytes once so they can both be cached to disk and
169 // streamed back, instead of calling imagepng() twice.
170 ob_start();
171 imagepng( $out );
172 $png_data = ob_get_clean();
173 imagedestroy( $out );
174
175 if ( $cache_path ) {
176 SocialIconImageCache::write( $cache_path, $png_data );
177 }
178
179 echo $png_data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
180 exit;
181 }
182
183 private function send_image_headers( $etag ) {
184 header( 'Content-Type: image/png' );
185 header( 'Cache-Control: public, max-age=31536000, immutable' );
186 header( 'ETag: ' . $etag );
187 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + YEAR_IN_SECONDS ) . ' GMT' );
188 }
189 }
190