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 |