| 1 |
<?php |
| 2 |
/** |
| 3 |
* Plugin Name: Attributes for Blocks |
| 4 |
* Plugin URI: https://wordpress.org/plugins/attributes-for-blocks |
| 5 |
* Description: Allows to add HTML attributes to Gutenberg blocks. |
| 6 |
* Version: 1.0.15 |
| 7 |
* Author: skadev |
| 8 |
* Author URI: https://profiles.wordpress.org/skadev/ |
| 9 |
*/ |
| 10 |
|
| 11 |
namespace wsd\afb; |
| 12 |
|
| 13 |
use WP_HTML_Tag_Processor; |
| 14 |
|
| 15 |
defined('ABSPATH') || exit; |
| 16 |
|
| 17 |
function_exists('get_plugin_data') || require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 18 |
$afb_plugin = get_plugin_data(__FILE__, false, false); |
| 19 |
define('WSD_AFB_VER', $afb_plugin['Version']); |
| 20 |
define('WSD_AFB_FILE', __FILE__); |
| 21 |
define('WSD_AFB_DIR', dirname(__FILE__)); |
| 22 |
|
| 23 |
/** |
| 24 |
* Blocks known to not work properly with Attributes for Blocks. |
| 25 |
* |
| 26 |
* @return array |
| 27 |
*/ |
| 28 |
function get_unsupported_blocks() { |
| 29 |
return apply_filters('afb_unsupported_blocks', [ |
| 30 |
'core/freeform', |
| 31 |
'core/html', |
| 32 |
'core/shortcode', |
| 33 |
'core/legacy-widget', |
| 34 |
]); |
| 35 |
} |
| 36 |
|
| 37 |
|
| 38 |
/** |
| 39 |
* Enqueue editor assets. |
| 40 |
*/ |
| 41 |
function editor_assets() { |
| 42 |
|
| 43 |
/** @var array */ |
| 44 |
$asset = include WSD_AFB_DIR . '/build/index.asset.php'; |
| 45 |
|
| 46 |
wp_enqueue_style( |
| 47 |
'attributes-for-blocks', |
| 48 |
plugins_url('build/style-index.css', WSD_AFB_FILE), |
| 49 |
[], |
| 50 |
$asset['version'], |
| 51 |
'all' |
| 52 |
); |
| 53 |
|
| 54 |
wp_enqueue_script( |
| 55 |
'attributes-for-blocks', |
| 56 |
plugins_url('build/index.js', WSD_AFB_FILE), |
| 57 |
$asset['dependencies'], |
| 58 |
$asset['version'], |
| 59 |
false |
| 60 |
); |
| 61 |
|
| 62 |
wp_localize_script( |
| 63 |
'attributes-for-blocks', |
| 64 |
'afbData', |
| 65 |
['unsupportedBlocks' => get_unsupported_blocks()] |
| 66 |
); |
| 67 |
|
| 68 |
if(function_exists('wp_set_script_translations')) { |
| 69 |
wp_set_script_translations('attributes-for-blocks', 'attributes-for-blocks'); |
| 70 |
} |
| 71 |
} |
| 72 |
add_action('enqueue_block_editor_assets', __NAMESPACE__ . '\\editor_assets', 5); |
| 73 |
|
| 74 |
|
| 75 |
/** |
| 76 |
* Should additional attributes from this plugin be applied to a block. |
| 77 |
* |
| 78 |
* @param mixed $attributes Block attributes. |
| 79 |
* @return bool |
| 80 |
*/ |
| 81 |
function has_attributes($attributes) { |
| 82 |
return is_array($attributes) |
| 83 |
&& isset($attributes['attributesForBlocks']) |
| 84 |
&& is_array($attributes['attributesForBlocks']) |
| 85 |
&& count($attributes['attributesForBlocks']) > 0; |
| 86 |
} |
| 87 |
|
| 88 |
|
| 89 |
/** |
| 90 |
* Handle merging custom attribute with existing attribute. |
| 91 |
* |
| 92 |
* @param string $attribute Attribute name. |
| 93 |
* @param string $current Current attribute value. |
| 94 |
* @param string $add Value to merge with. |
| 95 |
* @return string |
| 96 |
*/ |
| 97 |
function merge_attributes($attribute, $current, $add) { |
| 98 |
|
| 99 |
/** Clean attribute. */ |
| 100 |
switch($attribute) { |
| 101 |
case 'style': |
| 102 |
if($current === $add) { |
| 103 |
break; |
| 104 |
} |
| 105 |
/** Ensure style applied via JS matches input style. */ |
| 106 |
$add = str_replace(': ', ':', $add); |
| 107 |
$add = str_replace('; ', ';', $add); |
| 108 |
$add = rtrim($add, ';'); |
| 109 |
$current = rtrim($current, ';'); |
| 110 |
/** Fix `WP_HTML_Tag_Processor` stripping leading `-`, causing a mismatch. */ |
| 111 |
$current = str_replace('-afb-placeholder', 'afb-placeholder', $current); |
| 112 |
$current = str_replace('afb-placeholder', '-afb-placeholder', $current); |
| 113 |
break; |
| 114 |
} |
| 115 |
|
| 116 |
$current = trim( |
| 117 |
/** Remove the existing attribute value when it already exists (added via JS while the block also has PHP `render_callback`). */ |
| 118 |
str_replace($add, '', $current) |
| 119 |
); |
| 120 |
$add = trim($add); |
| 121 |
|
| 122 |
/** Nothing to merge. */ |
| 123 |
if(empty($current)) { |
| 124 |
return $add; |
| 125 |
} |
| 126 |
|
| 127 |
/** Determine the separator based on attribute type. */ |
| 128 |
$separator = ' '; |
| 129 |
switch($attribute) { |
| 130 |
case 'style': |
| 131 |
if(substr($current, -1) !== ';' && substr($add, 0, 1) !== ';') { |
| 132 |
$separator = ';'; |
| 133 |
} |
| 134 |
break; |
| 135 |
} |
| 136 |
/** @var string */ |
| 137 |
$separator = apply_filters('afb_attribute_separator', $separator, $attribute); |
| 138 |
|
| 139 |
return implode($separator, [$current, $add]); |
| 140 |
} |
| 141 |
|
| 142 |
|
| 143 |
/** |
| 144 |
* @param array $args AFB settings. |
| 145 |
* @param WP_HTML_Tag_Processor $tags |
| 146 |
* @return array Attribute name and value pairs. |
| 147 |
*/ |
| 148 |
function get_attributes($args, $tags) { |
| 149 |
|
| 150 |
$attributes = []; |
| 151 |
|
| 152 |
foreach($args as $key => $value) { |
| 153 |
|
| 154 |
/** Override attribute. */ |
| 155 |
if(strpos($key, '@') === 0) { |
| 156 |
$attributes[substr($key, 1)] = $value; |
| 157 |
continue; |
| 158 |
} |
| 159 |
|
| 160 |
/** Merge attribute. */ |
| 161 |
$attributes[$key] = merge_attributes( |
| 162 |
$key, |
| 163 |
$tags->get_attribute($key) ?? '', |
| 164 |
$value |
| 165 |
); |
| 166 |
} |
| 167 |
|
| 168 |
return apply_filters('afb_get_attributes', $attributes, $args, $tags); |
| 169 |
} |
| 170 |
|
| 171 |
|
| 172 |
/** |
| 173 |
* Add attributes to block root element. |
| 174 |
* |
| 175 |
* @param array $args AFB settings. |
| 176 |
* @param string $html Block HTML. |
| 177 |
* @return string Block HTML with additional attributes. |
| 178 |
*/ |
| 179 |
function add_attributes($args, $html) { |
| 180 |
|
| 181 |
$tags = new WP_HTML_Tag_Processor($html); |
| 182 |
if($tags->next_tag()) { |
| 183 |
|
| 184 |
/** Add attributes. */ |
| 185 |
foreach(get_attributes($args, $tags) as $key => $value) { |
| 186 |
$tags->set_attribute($key, $value); |
| 187 |
} |
| 188 |
|
| 189 |
return $tags->get_updated_html(); |
| 190 |
} |
| 191 |
|
| 192 |
return $html; |
| 193 |
} |
| 194 |
|
| 195 |
|
| 196 |
/** |
| 197 |
* When registering a block, add AFB argument and wrap `render_callback`. |
| 198 |
* |
| 199 |
* @param array $args |
| 200 |
* @param string $name |
| 201 |
* @return array |
| 202 |
*/ |
| 203 |
function block_args($args, $name) { |
| 204 |
|
| 205 |
static $not_supported; |
| 206 |
if(!is_array($not_supported)) { |
| 207 |
$not_supported = get_unsupported_blocks(); |
| 208 |
} |
| 209 |
|
| 210 |
if(in_array($name, $not_supported, true)) { |
| 211 |
return $args; |
| 212 |
} |
| 213 |
|
| 214 |
if(!isset($args['attributes']) || !is_array($args['attributes'])) { |
| 215 |
$args['attributes'] = []; |
| 216 |
} |
| 217 |
|
| 218 |
/** Register AFB attributes, this is necessary for `/wp-json/wp/v2/block-renderer` REST endpoint to not throw `rest_additional_properties_forbidden`. */ |
| 219 |
$args['attributes']['attributesForBlocks'] = [ |
| 220 |
'type' => 'object', |
| 221 |
'default' => [], |
| 222 |
]; |
| 223 |
|
| 224 |
return $args; |
| 225 |
} |
| 226 |
add_filter('register_block_type_args', __NAMESPACE__ . '\\block_args', 10, 2); |
| 227 |
|
| 228 |
|
| 229 |
/** |
| 230 |
* Add attributes to blocks' root HTML element when applicable. |
| 231 |
* |
| 232 |
* @param string $block_content Rendered block. |
| 233 |
* @param array $block Parsed array representation of block. |
| 234 |
* @return string |
| 235 |
*/ |
| 236 |
function render_block($block_content, $block) { |
| 237 |
|
| 238 |
static $not_supported; |
| 239 |
if(!is_array($not_supported)) { |
| 240 |
$not_supported = get_unsupported_blocks(); |
| 241 |
} |
| 242 |
|
| 243 |
if(in_array($block['blockName'], $not_supported, true)) { |
| 244 |
return $block_content; |
| 245 |
} |
| 246 |
|
| 247 |
if(!has_attributes($block['attrs'] ?? [])) { |
| 248 |
return $block_content; |
| 249 |
} |
| 250 |
|
| 251 |
return add_attributes($block['attrs']['attributesForBlocks'], $block_content); |
| 252 |
} |
| 253 |
add_filter('render_block', __NAMESPACE__ . '\\render_block', 10, 2); |
| 254 |
|
| 255 |
|
| 256 |
/** |
| 257 |
* Strips `attributesForBlocks` block attribute when the current user doesn't have `unfiltered_html` capabilities. |
| 258 |
* |
| 259 |
* @param string $content Content to filter through KSES. |
| 260 |
*/ |
| 261 |
function sanitize_attributes($content) { |
| 262 |
|
| 263 |
if(strpos($content, '<!-- wp:') === false) { // !has_blocks() |
| 264 |
return $content; |
| 265 |
} |
| 266 |
|
| 267 |
if(strpos($content, 'attributesForBlocks') === false) { |
| 268 |
return $content; |
| 269 |
} |
| 270 |
|
| 271 |
if(!defined('SECURE_AUTH_COOKIE')) { |
| 272 |
if(!function_exists('wp_cookie_constants')) { |
| 273 |
require ABSPATH . WPINC . '/default-constants.php'; |
| 274 |
} |
| 275 |
wp_cookie_constants(); |
| 276 |
} |
| 277 |
|
| 278 |
if(!function_exists('wp_get_current_user')) { |
| 279 |
require ABSPATH . WPINC . '/pluggable.php'; |
| 280 |
} |
| 281 |
|
| 282 |
if(current_user_can('unfiltered_html')) { |
| 283 |
return $content; |
| 284 |
} |
| 285 |
|
| 286 |
$matches = null; |
| 287 |
if(preg_match_all('/\\\?"attributesForBlocks\\\?"\s*:\s*{[^}]*}/', $content, $matches)) { |
| 288 |
|
| 289 |
foreach($matches[0] as $atts) { |
| 290 |
|
| 291 |
$empty_atts = '"attributesForBlocks":{}'; |
| 292 |
$was_slashed = wp_unslash($atts) !== $atts; |
| 293 |
if($was_slashed) { |
| 294 |
$empty_atts = wp_slash($empty_atts); |
| 295 |
} |
| 296 |
$content = str_replace($atts, $empty_atts, $content); |
| 297 |
} |
| 298 |
} |
| 299 |
|
| 300 |
return $content; |
| 301 |
} |
| 302 |
add_filter('pre_kses', __NAMESPACE__ . '\\sanitize_attributes'); |
| 303 |
|
| 304 |
|
| 305 |
/** |
| 306 |
* Add GitHub and Donate links on the plugins page. |
| 307 |
* |
| 308 |
* @param array $plugin_meta |
| 309 |
* @param string $plugin_file |
| 310 |
* @return array |
| 311 |
*/ |
| 312 |
function plugin_links($plugin_meta, $plugin_file) { |
| 313 |
if($plugin_file === plugin_basename(WSD_AFB_FILE)) { |
| 314 |
$plugin_meta[] = '<a href="https://github.com/ska-dev-1/attributes-for-blocks" target="_blank" rel="noopener noreferrer">GitHub</a>'; |
| 315 |
$plugin_meta[] = '<a href="https://buymeacoffee.com/skadev" target="_blank" rel="noopener noreferrer">Donate</a>'; |
| 316 |
} |
| 317 |
return $plugin_meta; |
| 318 |
} |
| 319 |
add_filter('plugin_row_meta', __NAMESPACE__ . '\\plugin_links', 10, 2); |
| 320 |
|