PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.7
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.7
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 days ago class-dynamic-block-assets.php 2 days ago class-recent-posts-controller.php 2 days ago class-table-of-contents-controller.php 2 days ago
class-author-box-controller.php
324 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. (string) cast on an object without __toString
40 if ($source === 'custom') {
41 $name = isset($attributes['authorName']) && is_scalar($attributes['authorName']) ? (string) $attributes['authorName'] : '';
42 $bio = isset($attributes['authorBio']) && is_scalar($attributes['authorBio']) ? (string) $attributes['authorBio'] : '';
43 } elseif ($user_id > 0) {
44 $name = (string) get_the_author_meta('display_name', $user_id);
45 $bio = (string) 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']) && is_string($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']) && is_string($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 <?php // $name may carry RichText markup for custom authors; strip tags so the alt text reads cleanly. ?>
108 alt="<?php echo esc_attr(wp_strip_all_tags($name)); ?>"
109 width="<?php echo esc_attr($size); ?>"
110 height="<?php echo esc_attr($size); ?>"
111 style="border-radius:<?php echo esc_attr($border_radius / 2); ?>%;width:<?php echo esc_attr($size); ?>px;" />
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 <?php // Linked branch only runs for dynamic sources, where $name is the plain display_name. esc_html() keeps it as text and avoids emitting an anchor inside this anchor. ?>
128 <a href="<?php echo esc_url($archive_url); ?>"><?php echo esc_html($name); ?></a>
129 <?php else : ?>
130 <?php
131 // Custom authors enter the name through a RichText field, which stores inline markup
132 // (bold, italic, links, etc.). wp_kses_post() sanitizes it while preserving that
133 // formatting; esc_html() would double-escape the tags so they render as literal text.
134 echo wp_kses_post($name);
135 ?>
136 <?php endif; ?>
137 </<?php echo esc_html($name_tag); ?>>
138 <?php endif; ?>
139
140 <?php if ($display_bio && $bio !== '') : ?>
141 <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;">
142 <?php
143 // The bio is a RichText field for custom authors (inline formatting, links, line breaks).
144 // wp_kses_post() preserves that markup safely; esc_html() would escape it into literal tags.
145 echo wp_kses_post($bio);
146 ?>
147 </p>
148 <?php endif; ?>
149
150 <?php if ($socials_html !== '') : ?>
151 <div class="superbaddons-authorbox-social-wrapper">
152 <?php
153 // SAFE OUTPUT: $socials_html contains only HTML produced by trusted WordPress core code, never raw user input.
154 // It is the concatenation of:
155 // 1. The output of WordPress core's render_block() for the inner core/social-links block
156 // (passed in as $content by core, or built via render_block() in renderLegacySocials()).
157 // User-supplied attributes (service name, URL) are sanitized by core's own block render
158 // callbacks before being inserted into the markup. We do not concatenate any user input here.
159 // 2. An optional website link built in renderSocials() using esc_url() on the href and
160 // esc_html() on the visible text.
161 // Escaping with wp_kses_post() is NOT safe here: core/social-links emits inline <svg> icons,
162 // and wp_kses_post()'s allowlist strips <svg> entirely, which would render empty social links.
163 // This matches the pattern WordPress core itself uses to output rendered inner block content.
164 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
165 echo $socials_html;
166 ?>
167 </div>
168 <?php endif; ?>
169 </div>
170 </div>
171 <?php
172 return ob_get_clean();
173 } catch (Exception $ex) {
174 LogController::HandleException($ex);
175 return '';
176 }
177 }
178
179 private static function resolveUserId($source, $attributes, $block)
180 {
181 if ($source === 'currentPost') {
182 $post_id = 0;
183 // $block is normally a WP_Block instance, but defensive checks guard
184 // against plugins that invoke our render_callback with unexpected shapes.
185 if (is_object($block) && isset($block->context) && is_array($block->context) && isset($block->context['postId'])) {
186 $post_id = intval($block->context['postId']);
187 }
188 if ($post_id <= 0) {
189 return 0;
190 }
191 $author = get_post_field('post_author', $post_id);
192 return $author ? intval($author) : 0;
193 }
194 if ($source === 'user') {
195 return isset($attributes['authorUserId']) ? intval($attributes['authorUserId']) : 0;
196 }
197 return 0;
198 }
199
200 private static function resolveAvatarUrl($source, $user_id, $attributes, $size)
201 {
202 if ($source === 'custom') {
203 $custom_id = isset($attributes['customAuthorImageId']) ? intval($attributes['customAuthorImageId']) : 0;
204 if ($custom_id > 0) {
205 $url = wp_get_attachment_image_url($custom_id, array($size, $size));
206 if ($url) {
207 return $url;
208 }
209 }
210 // is_string guards against esc_url_raw being handed an array/object
211 if (!empty($attributes['customAuthorImage']) && is_string($attributes['customAuthorImage'])) {
212 return esc_url_raw($attributes['customAuthorImage']);
213 }
214 if (!empty($attributes['authorImage']) && is_string($attributes['authorImage'])) {
215 return esc_url_raw($attributes['authorImage']);
216 }
217 return '';
218 }
219 if ($user_id > 0) {
220 $url = get_avatar_url($user_id, array('size' => $size));
221 return $url ? $url : '';
222 }
223 return '';
224 }
225
226 private static function buildColorStyle($attributes)
227 {
228 $parts = array();
229
230 // is_string guards every WPC slug before sanitize_html_class/concat;
231 // Raw color values are routed through isValidColor() which already
232 // validates the input is a string.
233 $name_wpc = isset($attributes['colorAuthorNameWPC']) && is_string($attributes['colorAuthorNameWPC']) ? $attributes['colorAuthorNameWPC'] : '';
234 $name_raw = isset($attributes['colorAuthorName']) ? $attributes['colorAuthorName'] : '';
235 if (!empty($name_wpc)) {
236 $parts[] = '--superb-authorbox-name-color:var(--wp--preset--color--' . sanitize_html_class($name_wpc) . ')';
237 } elseif (!empty($name_raw) && self::isValidColor($name_raw)) {
238 $parts[] = '--superb-authorbox-name-color:' . $name_raw;
239 }
240
241 $bio_wpc = isset($attributes['colorAuthorBioWPC']) && is_string($attributes['colorAuthorBioWPC']) ? $attributes['colorAuthorBioWPC'] : '';
242 $bio_raw = isset($attributes['colorAuthorBio']) ? $attributes['colorAuthorBio'] : '';
243 if (!empty($bio_wpc)) {
244 $parts[] = '--superb-authorbox-bio-color:var(--wp--preset--color--' . sanitize_html_class($bio_wpc) . ')';
245 } elseif (!empty($bio_raw) && self::isValidColor($bio_raw)) {
246 $parts[] = '--superb-authorbox-bio-color:' . $bio_raw;
247 }
248
249 // Trailing semicolon matters: get_block_wrapper_attributes() concatenates
250 // this with the block-supports style string (background, border, etc.).
251 // Without it, the last declaration runs into the next one and the CSS
252 // parser drops the combined invalid declaration.
253 return empty($parts) ? '' : implode(';', $parts) . ';';
254 }
255
256 private static function isValidColor($value)
257 {
258 if (!is_string($value)) {
259 return false;
260 }
261 return (bool) preg_match('/^(#[0-9a-f]{3,8}|(?:rgb|hsl|oklch|oklab|color)\([^;{}]*\))$/i', $value);
262 }
263
264 /**
265 * Build the social row HTML.
266 *
267 * For migrated/new blocks $content is the pre-rendered core/social-links
268 * InnerBlock (WP renders inner_blocks into $content before calling us).
269 * Legacy 0.2.0 blocks that were never re-saved have no inner_blocks and
270 * $content is the old static layout HTML — we ignore it and render fresh
271 * icons from the legacy socialsLink* attributes so users still see them
272 * on the frontend until the editor migration runs.
273 */
274 private static function renderSocials($content, $block, $attributes, $website_url)
275 {
276 $has_inner_blocks = $block !== null && is_object($block) && !empty($block->inner_blocks);
277 $socials = $has_inner_blocks && is_string($content) ? $content : '';
278
279 if (trim($socials) === '') {
280 $socials = self::renderLegacySocials($attributes);
281 }
282
283 $website = '';
284 if (!empty($website_url)) {
285 $website = '<a class="superbaddons-authorbox-author-website" href="' . esc_url($website_url) . '" rel="noopener noreferrer" target="_blank">' . esc_html($website_url) . '</a>';
286 }
287
288 if (trim($socials) === '' && $website === '') {
289 return '';
290 }
291 return $socials . $website;
292 }
293
294 private static function renderLegacySocials($attributes)
295 {
296 $children = array();
297 foreach (self::$legacy_socials as $attr_key => $service) {
298 // Legacy URLs were saved as strings; reject anything else outright
299 $url = isset($attributes[$attr_key]) && is_string($attributes[$attr_key]) ? trim($attributes[$attr_key]) : '';
300 if ($url === '') {
301 continue;
302 }
303 $children[] = array(
304 'blockName' => 'core/social-link',
305 'attrs' => array('service' => $service, 'url' => $url),
306 'innerBlocks' => array(),
307 'innerHTML' => '',
308 'innerContent' => array(),
309 );
310 }
311 if (empty($children)) {
312 return '';
313 }
314 $block = array(
315 'blockName' => 'core/social-links',
316 'attrs' => array(),
317 'innerBlocks' => $children,
318 'innerHTML' => '',
319 'innerContent' => array_merge(array(null), array_fill(0, count($children), null)),
320 );
321 return render_block($block);
322 }
323 }
324