class-social-links.php
<?php
/**
* This file is part of the WooCommerce Email Editor package
*
* @package Automattic\WooCommerce\EmailEditor
*/
declare( strict_types = 1 );
namespace Automattic\WooCommerce\EmailEditor\Integrations\Core\Renderer\Blocks;
use Automattic\WooCommerce\EmailEditor\Engine\Settings_Controller;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Social_Links_Helper;
/**
* Renders the social links block.
*/
class Social_Links extends Abstract_Block_Renderer {
/**
* Cache of the core social link services.
*
* @var array<string, array>
*/
private $core_social_link_services_cache = array();
/**
* Supported image types.
*
* @var array<string>
*/
private $supported_image_types = array( 'white', 'brand' );
/**
* Renders the block content.
*
* @param string $block_content Block content.
* @param array $parsed_block Parsed block.
* @param Settings_Controller $settings_controller Settings controller.
* @return string
*/
protected function render_content( $block_content, array $parsed_block, Settings_Controller $settings_controller ): string {
$attrs = $parsed_block['attrs'] ?? array();
$inner_blocks = $parsed_block['innerBlocks'] ?? array();
$content = '';
foreach ( $inner_blocks as $block ) {
$content .= $this->generate_social_link_content( $block, $attrs );
}
return str_replace(
'{social_links_content}',
$content,
$this->get_block_wrapper( $block_content, $parsed_block )
);
}
/**
* Generates the social link content.
*
* @param array $block The block data.
* @param array $parent_block_attrs The parent block attributes.
* @return string The generated content.
*/
private function generate_social_link_content( $block, $parent_block_attrs ) {
$service_name = $block['attrs']['service'] ?? '';
$service_url = $block['attrs']['url'] ?? '';
$label = $block['attrs']['label'] ?? '';
if ( empty( $service_name ) || empty( $service_url ) ) {
return '';
}
/**
* Prepend emails with `mailto:` if not set.
* The `is_email` returns false for emails with schema.
*/
if ( is_email( $service_url ) ) {
$service_url = 'mailto:' . antispambot( $service_url );
}
/**
* Prepend URL with https:// if it doesn't appear to contain a scheme
* and it's not a relative link or a fragment.
*/
if ( ! wp_parse_url( $service_url, PHP_URL_SCHEME ) && ! str_starts_with( $service_url, '//' ) && ! str_starts_with( $service_url, '#' ) ) {
$service_url = 'https://' . $service_url;
}
$open_in_new_tab = $parent_block_attrs['openInNewTab'] ?? false;
$show_labels = $parent_block_attrs['showLabels'] ?? false;
$size = $parent_block_attrs['size'] ?? Social_Links_Helper::get_default_social_link_size();
$service_brand_color = Social_Links_Helper::get_service_brand_color( $service_name );
$icon_color_value = $parent_block_attrs['iconColorValue'] ?? '#ffffff'; // use white as default icon color.
$icon_background_color_value = $parent_block_attrs['iconBackgroundColorValue'] ?? '';
$is_logos_only = strpos( $parent_block_attrs['className'] ?? '', 'is-style-logos-only' ) !== false;
$is_pill_shape = strpos( $parent_block_attrs['className'] ?? '', 'is-style-pill-shape' ) !== false;
if ( ! $is_logos_only && Social_Links_Helper::detect_whiteish_color( $icon_color_value ) && ( Social_Links_Helper::detect_whiteish_color( $icon_background_color_value ) || empty( $icon_background_color_value ) ) ) {
// If the icon color is white and the background color is white or empty, use the service brand color for the icon background color.
$icon_background_color_value = ! empty( $service_brand_color ) ? $service_brand_color : '#000';
}
if ( $is_logos_only ) {
// logos only mode does not need background color. We also don't really need the icon color (we can't change png image color anyways).
// We set it so that the label text color will reflect the service brand color.
$icon_color_value = ! empty( $service_brand_color ) ? $service_brand_color : '#000';
}
$icon_size = Social_Links_Helper::get_social_link_size_option_value( $size );
$service_icon_url = $this->get_service_icon_url( $service_name, $is_logos_only ? 'brand' : 'white' );
$service_label = '';
if ( $show_labels ) {
$text = ! empty( $label ) ? trim( $label ) : '';
$service_label = $text ? $text : block_core_social_link_get_name( $service_name );
}
$main_table_styles = $this->compile_css(
array(
'background-color' => $icon_background_color_value,
'border-radius' => '9999px',
'display' => 'inline-table',
'float' => 'none',
)
);
// divide the icon value by 2 to get the font size.
$font_size_value = (int) rtrim( $icon_size, 'px' );
$font_size = ( $font_size_value / 2 ) + 1; // inline with core styles.
$text_font_size = "{$font_size}px";
$anchor_styles = $this->compile_css(
array(
'color' => $icon_color_value,
'text-decoration' => 'none',
'text-transform' => 'none',
'font-size' => $text_font_size,
)
);
$anchor_html = sprintf( ' style="%s" ', esc_attr( $anchor_styles ) );
if ( $open_in_new_tab ) {
$anchor_html .= ' rel="noopener nofollow" target="_blank" ';
}
$row_container_styles = array(
'display' => 'block',
'padding' => '0.25em',
);
if ( $is_pill_shape ) {
$row_container_styles['padding-left'] = '17px';
$row_container_styles['padding-right'] = '17px';
}
$row_container_styles = $this->compile_css( $row_container_styles );
// rendering inspired by mjml social. https://documentation.mjml.io/#mj-social.
return sprintf(
'
<!--[if mso | IE]><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="%1$s">
<tbody><tr style="%7$s">
<td style="vertical-align:middle;font-size:%9$s">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="">
<tbody><tr>
<td style="vertical-align:middle;">
<a href="%2$s" %5$s class="wp-block-social-link-anchor">
<img height="%8$s" src="%3$s" style="display:block;" width="%8$s" alt="%4$s">
</a>
</td>
</tr>
</tbody></table>
</td>
' . ( $service_label ? '
<td style="vertical-align:middle;padding-left:6px;padding-right:6px;font-size:%9$s">
<a href="%2$s" %5$s class="wp-block-social-link-anchor">
<span style="margin-left:.5em;margin-right:.5em"> %6$s </span>
</a>
</td>
' : '' ) . '
</tr>
</tbody></table>
<!--[if mso | IE]></td><![endif]-->
',
esc_attr( $main_table_styles ), // %1$s -> The main table styles.
esc_url( $service_url ), // %2$s -> The a href link.
esc_url( $service_icon_url ), // %3$s -> The Img src.
// translators: %s is the social service name.
sprintf( __( '%s icon', 'woocommerce' ), $service_name ), // %4$s -> The Img alt.
$anchor_html, // %5$s -> The a styles plus rel and target attributes.
esc_html( $service_label ), // %6$s -> The a text (label).
esc_attr( $row_container_styles ), // %7$s -> The tr row container styles.
esc_attr( $icon_size ), // %8$s -> The icon size.
esc_attr( $text_font_size ) // %9$s -> The text font size.
);
}
/**
* Gets the block wrapper.
*
* @param string $block_content The block content.
* @param array $parsed_block The parsed block.
* @return string The block wrapper HTML.
*/
private function get_block_wrapper( $block_content, $parsed_block ) {
$content = $this->adjust_block_content( $block_content, $parsed_block );
$table_styles = $content['table_styles'];
$classes = $content['classes'];
$compiled_styles = $content['compiled_styles'];
$align = $content['align'];
return sprintf(
'<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
<table class="wp-block-social-links" style="%1$s vertical-align:top;" border="0" width="100%%" cellpadding="0" cellspacing="0" role="presentation">
<tr role="presentation">
<td class="%2$s" style="%3$s" align="%4$s" role="presentation">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><![endif]-->
%5$s
<!--[if mso | IE]></tr></table><![endif]-->
</td>
</tr>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->',
esc_attr( $table_styles ),
esc_attr( $classes ),
esc_attr( $compiled_styles ),
esc_attr( $align ),
'{social_links_content}'
);
}
/**
* Adjusts the block content.
* Returns css classes and styles compatible with email clients.
*
* @param string $block_content The block content.
* @param array $parsed_block The parsed block.
* @return array The adjusted block content.
*/
private function adjust_block_content( $block_content, $parsed_block ) {
$block_content = $this->adjust_style_attribute( $block_content );
$block_attributes = wp_parse_args(
$parsed_block['attrs'] ?? array(),
array(
'textAlign' => 'left',
'style' => array(),
)
);
$html = new \WP_HTML_Tag_Processor( $block_content );
$classes = 'wp-block-social-links';
if ( $html->next_tag() ) {
/** @var string $block_classes */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$block_classes = $html->get_attribute( 'class' ) ?? '';
$classes .= ' ' . $block_classes;
// remove has-background to prevent double padding applied for wrapper and inner element.
$block_classes = str_replace( 'has-background', '', $block_classes );
// remove border related classes because we handle border on wrapping table cell.
$block_classes = preg_replace( '/[a-z-]+-border-[a-z-]+/', '', $block_classes );
/** @var string $block_classes */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$html->set_attribute( 'class', trim( $block_classes ) );
$block_content = $html->get_updated_html();
}
$block_styles = $this->get_styles_from_block(
array(
'color' => $block_attributes['style']['color'] ?? array(),
'spacing' => $block_attributes['style']['spacing'] ?? array(),
'typography' => $block_attributes['style']['typography'] ?? array(),
'border' => $block_attributes['style']['border'] ?? array(),
)
);
$styles = array(
'min-width' => '100%', // prevent Gmail App from shrinking the table on mobile devices.
'vertical-align' => 'middle',
'word-break' => 'break-word',
);
$styles['text-align'] = 'left';
if ( ! empty( $parsed_block['attrs']['textAlign'] ) ) { // in this case, textAlign needs to be one of 'left', 'center', 'right'.
$styles['text-align'] = $parsed_block['attrs']['textAlign'];
} elseif ( in_array( $parsed_block['attrs']['align'] ?? null, array( 'left', 'center', 'right' ), true ) ) {
$styles['text-align'] = $parsed_block['attrs']['align'];
}
$compiled_styles = $this->compile_css( $block_styles['declarations'], $styles );
$table_styles = 'border-collapse: separate;'; // Needed because of border radius.
return array(
'table_styles' => $table_styles,
'classes' => $classes,
'compiled_styles' => $compiled_styles,
'align' => $styles['text-align'],
'block_content' => $block_content,
);
}
/**
* 1) We need to remove padding because we render padding on wrapping table cell
* 2) We also need to replace font-size to avoid clamp() because clamp() is not supported in many email clients.
* The font size values is automatically converted to clamp() when WP site theme is configured to use fluid layouts.
* Currently (WP 6.5), there is no way to disable this behavior.
*
* @param string $block_content Block content.
*/
private function adjust_style_attribute( string $block_content ): string {
$html = new \WP_HTML_Tag_Processor( $block_content );
if ( $html->next_tag() ) {
$element_style_value = $html->get_attribute( 'style' );
$element_style = isset( $element_style_value ) ? strval( $element_style_value ) : '';
// Padding may contain value like 10px or variable like var(--spacing-10).
$element_style = preg_replace( '/padding[^:]*:.?[0-9a-z-()]+;?/', '', $element_style );
// Remove border styles. We apply border styles on the wrapping table cell.
$element_style = preg_replace( '/border[^:]*:.?[0-9a-z-()#]+;?/', '', strval( $element_style ) );
// We define the font-size on the wrapper element, but we need to keep font-size definition here
// to prevent CSS Inliner from adding a default value and overriding the value set by user, which is on the wrapper element.
// The value provided by WP uses clamp() function which is not supported in many email clients.
$element_style = preg_replace( '/font-size:[^;]+;?/', 'font-size: inherit;', strval( $element_style ) );
/** @var string $element_style */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- used for phpstan
$html->set_attribute( 'style', esc_attr( $element_style ) );
$block_content = $html->get_updated_html();
}
return $block_content;
}
/**
* Gets the service icon URL.
*
* Default image type is 'white'.
*
* @param string $service The service name.
* @param string $image_type The image type. e.g 'white', 'brand'.
* @return string The service icon URL.
*/
public function get_service_icon_url( $service, $image_type = '' ) {
$image_type = empty( $image_type ) ? 'white' : $image_type;
$service = empty( $service ) ? '' : strtolower( $service );
if ( empty( $this->core_social_link_services_cache ) ) {
$services = block_core_social_link_services();
$this->core_social_link_services_cache = is_array( $services ) ? $services : array();
}
if ( ! isset( $this->core_social_link_services_cache[ $service ] ) ) {
// not in the list of core services.
return '';
}
if ( ! in_array( $image_type, $this->supported_image_types, true ) ) {
return '';
}
// Get URL to icons/service.png.
$service_icon_url = $this->get_service_png_url( $service, $image_type );
if ( $service_icon_url && ! file_exists( $this->get_service_png_path( $service, $image_type ) ) ) {
// The image file does not exist.
return '';
}
return $service_icon_url;
}
/**
* Gets the service PNG URL.
*
* @param string $service The service name.
* @param string $image_type The image type. e.g 'white', 'brand'.
* @return string The service PNG URL.
*/
public function get_service_png_url( $service, $image_type = 'white' ) {
if ( empty( $service ) ) {
return '';
}
$image_type = empty( $image_type ) ? 'white' : $image_type;
$file_name = "/icons/{$service}/{$service}-{$image_type}.png";
return plugins_url( $file_name, __FILE__ );
}
/**
* Gets the service PNG path.
*
* @param string $service The service name.
* @param string $image_type The image type. e.g 'white', 'brand'.
* @return string The service PNG path.
*/
public function get_service_png_path( $service, $image_type = 'white' ) {
if ( empty( $service ) ) {
return '';
}
$image_type = empty( $image_type ) ? 'white' : $image_type;
$file_name = "/icons/{$service}/{$service}-{$image_type}.png";
return __DIR__ . $file_name;
}
}