PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / block-api / class-author-box-controller.php
superb-blocks / src / gutenberg / block-api Last commit date
class-author-box-controller.php 2 months ago class-dynamic-block-assets.php 2 months ago class-recent-posts-controller.php 2 months ago class-table-of-contents-controller.php 2 months ago
class-author-box-controller.php
307 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\BlocksAPI\Controllers;
4
5 use SuperbAddons\Data\Controllers\LogController;
6 use Exception;
7
8 defined('ABSPATH') || exit();
9
10 class AuthorBoxController
11 {
12 private static $allowed_name_tags = array('p', 'h2', 'h3', 'h4');
13 private static $legacy_socials = array(
14 'socialsLinkFacebook' => 'facebook',
15 'socialsLinkInstagram' => 'instagram',
16 'socialsLinkX' => 'x',
17 'socialsLinkLinkedin' => 'linkedin',
18 );
19
20 public static function Render($attributes, $content, $block = null)
21 {
22 try {
23 $attributes = is_array($attributes) ? $attributes : array();
24
25 $source = isset($attributes['authorSource']) ? $attributes['authorSource'] : 'custom';
26 if (!in_array($source, array('custom', 'user', 'currentPost'), true)) {
27 $source = 'custom';
28 }
29
30 $size = isset($attributes['avatarSize']) ? intval($attributes['avatarSize']) : 96;
31 if ($size < 24) {
32 $size = 24;
33 } elseif ($size > 96) {
34 $size = 96;
35 }
36
37 $user_id = self::resolveUserId($source, $attributes, $block);
38
39 // Resolve display values
40 if ($source === 'custom') {
41 $name = isset($attributes['authorName']) ? (string) $attributes['authorName'] : '';
42 $bio = isset($attributes['authorBio']) ? (string) $attributes['authorBio'] : '';
43 } elseif ($user_id > 0) {
44 $name = get_the_author_meta('display_name', $user_id);
45 $bio = get_the_author_meta('description', $user_id);
46 } else {
47 $name = '';
48 $bio = '';
49 }
50
51 $avatar_url = self::resolveAvatarUrl($source, $user_id, $attributes, $size);
52 $archive_url = '';
53 if (!empty($attributes['linkNameToArchive']) && $user_id > 0) {
54 $archive_url = get_author_posts_url($user_id);
55 }
56 $website_url = '';
57 if ($source !== 'custom' && $user_id > 0) {
58 $website_url = get_the_author_meta('user_url', $user_id);
59 }
60
61 $alignment = isset($attributes['toolbarAlignment']) ? $attributes['toolbarAlignment'] : 'left';
62 if (!in_array($alignment, array('left', 'center', 'right'), true)) {
63 $alignment = 'left';
64 }
65
66 $name_tag = isset($attributes['nameTagName']) ? $attributes['nameTagName'] : 'p';
67 if (!in_array($name_tag, self::$allowed_name_tags, true)) {
68 $name_tag = 'p';
69 }
70
71 $display_name = !isset($attributes['displayName']) || (bool) $attributes['displayName'];
72 $display_bio = !isset($attributes['displayBio']) || (bool) $attributes['displayBio'];
73 $display_socials = !isset($attributes['displaySocials']) || (bool) $attributes['displaySocials'];
74
75 $avatar_enabled = !isset($attributes['avatarEnabled']) || (bool) $attributes['avatarEnabled'];
76 $border_radius = isset($attributes['avatarBorderRadius']) ? intval($attributes['avatarBorderRadius']) : 100;
77 $font_name = isset($attributes['fontSizeAuthorName']) ? intval($attributes['fontSizeAuthorName']) : 32;
78 $font_bio = isset($attributes['fontSizeAuthorBio']) ? intval($attributes['fontSizeAuthorBio']) : 14;
79
80 // Wrapper colors via CSS variables (mirrors editor preview).
81 $color_style = self::buildColorStyle($attributes);
82 $wrapper_extra = array(
83 'class' => 'superbaddons-authorbox superbaddons-authorbox-alignment-' . $alignment,
84 );
85 if ($color_style !== '') {
86 $wrapper_extra['style'] = $color_style;
87 }
88 $wrapper_attributes = get_block_wrapper_attributes($wrapper_extra);
89
90 $socials_html = '';
91 if ($display_socials) {
92 $socials_html = self::renderSocials($content, $block, $attributes, $website_url);
93 }
94
95 ob_start();
96 ?>
97 <div <?php
98 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() returns pre-escaped HTML attribute markup per WP core API.
99 echo $wrapper_attributes;
100 ?>>
101 <?php if ($avatar_enabled) : ?>
102 <div class="superbaddons-authorbox-left">
103 <?php if ($avatar_url !== '') : ?>
104 <img
105 class="superbaddons-authorbox-avatar"
106 src="<?php echo esc_url($avatar_url); ?>"
107 alt="<?php echo esc_attr($name); ?>"
108 width="<?php echo esc_attr($size); ?>"
109 height="<?php echo esc_attr($size); ?>"
110 style="border-radius:<?php echo esc_attr($border_radius / 2); ?>%;width:<?php echo esc_attr($size); ?>px;"
111 />
112 <?php endif; ?>
113 </div>
114 <?php endif; ?>
115
116 <div class="superbaddons-authorbox-right">
117 <?php if ($display_name && $name !== '') : ?>
118 <?php
119 $name_class = 'superbaddons-authorbox-authorname';
120 if ($archive_url !== '') {
121 $name_class .= ' superbaddons-authorbox-authorname-linked';
122 }
123 $name_style = 'font-size:' . $font_name . 'px;line-height:' . ($font_name + 8) . 'px;';
124 ?>
125 <<?php echo esc_html($name_tag); ?> class="<?php echo esc_attr($name_class); ?>" style="<?php echo esc_attr($name_style); ?>">
126 <?php if ($archive_url !== '') : ?>
127 <a href="<?php echo esc_url($archive_url); ?>"><?php echo esc_html($name); ?></a>
128 <?php else : ?>
129 <?php echo esc_html($name); ?>
130 <?php endif; ?>
131 </<?php echo esc_html($name_tag); ?>>
132 <?php endif; ?>
133
134 <?php if ($display_bio && $bio !== '') : ?>
135 <p class="superbaddons-authorbox-authorbio" style="font-size:<?php echo esc_attr($font_bio); ?>px;line-height:<?php echo esc_attr($font_bio + 5); ?>px;">
136 <?php echo esc_html($bio); ?>
137 </p>
138 <?php endif; ?>
139
140 <?php if ($socials_html !== '') : ?>
141 <div class="superbaddons-authorbox-social-wrapper">
142 <?php
143 // SAFE OUTPUT: $socials_html contains only HTML produced by trusted WordPress core code, never raw user input.
144 // It is the concatenation of:
145 // 1. The output of WordPress core's render_block() for the inner core/social-links block
146 // (passed in as $content by core, or built via render_block() in renderLegacySocials()).
147 // User-supplied attributes (service name, URL) are sanitized by core's own block render
148 // callbacks before being inserted into the markup. We do not concatenate any user input here.
149 // 2. An optional website link built in renderSocials() using esc_url() on the href and
150 // esc_html() on the visible text.
151 // Escaping with wp_kses_post() is NOT safe here: core/social-links emits inline <svg> icons,
152 // and wp_kses_post()'s allowlist strips <svg> entirely, which would render empty social links.
153 // This matches the pattern WordPress core itself uses to output rendered inner block content.
154 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
155 echo $socials_html;
156 ?>
157 </div>
158 <?php endif; ?>
159 </div>
160 </div>
161 <?php
162 return ob_get_clean();
163 } catch (Exception $ex) {
164 LogController::HandleException($ex);
165 return '';
166 }
167 }
168
169 private static function resolveUserId($source, $attributes, $block)
170 {
171 if ($source === 'currentPost') {
172 $post_id = 0;
173 if ($block !== null && isset($block->context) && isset($block->context['postId'])) {
174 $post_id = intval($block->context['postId']);
175 }
176 if ($post_id <= 0) {
177 return 0;
178 }
179 $author = get_post_field('post_author', $post_id);
180 return $author ? intval($author) : 0;
181 }
182 if ($source === 'user') {
183 return isset($attributes['authorUserId']) ? intval($attributes['authorUserId']) : 0;
184 }
185 return 0;
186 }
187
188 private static function resolveAvatarUrl($source, $user_id, $attributes, $size)
189 {
190 if ($source === 'custom') {
191 $custom_id = isset($attributes['customAuthorImageId']) ? intval($attributes['customAuthorImageId']) : 0;
192 if ($custom_id > 0) {
193 $url = wp_get_attachment_image_url($custom_id, array($size, $size));
194 if ($url) {
195 return $url;
196 }
197 }
198 if (!empty($attributes['customAuthorImage'])) {
199 return esc_url_raw($attributes['customAuthorImage']);
200 }
201 if (!empty($attributes['authorImage'])) {
202 return esc_url_raw($attributes['authorImage']);
203 }
204 return '';
205 }
206 if ($user_id > 0) {
207 $url = get_avatar_url($user_id, array('size' => $size));
208 return $url ? $url : '';
209 }
210 return '';
211 }
212
213 private static function buildColorStyle($attributes)
214 {
215 $parts = array();
216
217 $name_wpc = isset($attributes['colorAuthorNameWPC']) ? $attributes['colorAuthorNameWPC'] : '';
218 $name_raw = isset($attributes['colorAuthorName']) ? $attributes['colorAuthorName'] : '';
219 if (!empty($name_wpc)) {
220 $parts[] = '--superb-authorbox-name-color:var(--wp--preset--color--' . sanitize_html_class($name_wpc) . ')';
221 } elseif (!empty($name_raw) && self::isValidColor($name_raw)) {
222 $parts[] = '--superb-authorbox-name-color:' . $name_raw;
223 }
224
225 $bio_wpc = isset($attributes['colorAuthorBioWPC']) ? $attributes['colorAuthorBioWPC'] : '';
226 $bio_raw = isset($attributes['colorAuthorBio']) ? $attributes['colorAuthorBio'] : '';
227 if (!empty($bio_wpc)) {
228 $parts[] = '--superb-authorbox-bio-color:var(--wp--preset--color--' . sanitize_html_class($bio_wpc) . ')';
229 } elseif (!empty($bio_raw) && self::isValidColor($bio_raw)) {
230 $parts[] = '--superb-authorbox-bio-color:' . $bio_raw;
231 }
232
233 // Trailing semicolon matters: get_block_wrapper_attributes() concatenates
234 // this with the block-supports style string (background, border, etc.).
235 // Without it, the last declaration runs into the next one and the CSS
236 // parser drops the combined invalid declaration.
237 return empty($parts) ? '' : implode(';', $parts) . ';';
238 }
239
240 private static function isValidColor($value)
241 {
242 if (!is_string($value)) {
243 return false;
244 }
245 return (bool) preg_match('/^(#[0-9a-f]{3,8}|(?:rgb|hsl|oklch|oklab|color)\([^;{}]*\))$/i', $value);
246 }
247
248 /**
249 * Build the social row HTML.
250 *
251 * For migrated/new blocks $content is the pre-rendered core/social-links
252 * InnerBlock (WP renders inner_blocks into $content before calling us).
253 * Legacy 0.2.0 blocks that were never re-saved have no inner_blocks and
254 * $content is the old static layout HTML — we ignore it and render fresh
255 * icons from the legacy socialsLink* attributes so users still see them
256 * on the frontend until the editor migration runs.
257 */
258 private static function renderSocials($content, $block, $attributes, $website_url)
259 {
260 $has_inner_blocks = $block !== null && !empty($block->inner_blocks);
261 $socials = $has_inner_blocks && is_string($content) ? $content : '';
262
263 if (trim($socials) === '') {
264 $socials = self::renderLegacySocials($attributes);
265 }
266
267 $website = '';
268 if (!empty($website_url)) {
269 $website = '<a class="superbaddons-authorbox-author-website" href="' . esc_url($website_url) . '" rel="noopener noreferrer" target="_blank">' . esc_html($website_url) . '</a>';
270 }
271
272 if (trim($socials) === '' && $website === '') {
273 return '';
274 }
275 return $socials . $website;
276 }
277
278 private static function renderLegacySocials($attributes)
279 {
280 $children = array();
281 foreach (self::$legacy_socials as $attr_key => $service) {
282 $url = isset($attributes[$attr_key]) ? trim((string) $attributes[$attr_key]) : '';
283 if ($url === '') {
284 continue;
285 }
286 $children[] = array(
287 'blockName' => 'core/social-link',
288 'attrs' => array('service' => $service, 'url' => $url),
289 'innerBlocks' => array(),
290 'innerHTML' => '',
291 'innerContent' => array(),
292 );
293 }
294 if (empty($children)) {
295 return '';
296 }
297 $block = array(
298 'blockName' => 'core/social-links',
299 'attrs' => array(),
300 'innerBlocks' => $children,
301 'innerHTML' => '',
302 'innerContent' => array_merge(array(null), array_fill(0, count($children), null)),
303 );
304 return render_block($block);
305 }
306 }
307