| 1 |
<?php |
| 2 |
if (!defined('ABSPATH')) die('No direct access allowed'); |
| 3 |
|
| 4 |
if (!class_exists('WPO_WP_Kses_Compatibility')) : |
| 5 |
|
| 6 |
/** |
| 7 |
* Adds compatibility for WP KSES plugin. |
| 8 |
*/ |
| 9 |
class WPO_WP_Kses_Compatibility { |
| 10 |
|
| 11 |
/** |
| 12 |
* Explicit list of data-* attributes used in plugin's HTML output. |
| 13 |
* |
| 14 |
* @var array<string> |
| 15 |
*/ |
| 16 |
private static $allowed_attrs = array( |
| 17 |
'data-alt-label', |
| 18 |
'data-attachment-id', |
| 19 |
'data-background', |
| 20 |
'data-background-image', |
| 21 |
'data-blog', |
| 22 |
'data-blog_id', |
| 23 |
'data-colname', |
| 24 |
'data-count', |
| 25 |
'data-disabled', |
| 26 |
'data-embed', |
| 27 |
'data-enable', |
| 28 |
'data-filename', |
| 29 |
'data-id', |
| 30 |
'data-iframe-attr', |
| 31 |
'data-label', |
| 32 |
'data-max', |
| 33 |
'data-menuslug', |
| 34 |
'data-mode', |
| 35 |
'data-no-image-dimensions', |
| 36 |
'data-optimizable', |
| 37 |
'data-optimization', |
| 38 |
'data-optimization_id', |
| 39 |
'data-optimization_run_sort_order', |
| 40 |
'data-page', |
| 41 |
'data-post_id', |
| 42 |
'data-saveas', |
| 43 |
'data-sort', |
| 44 |
'data-src', |
| 45 |
'data-srcset', |
| 46 |
'data-tab', |
| 47 |
'data-table', |
| 48 |
'data-tablename', |
| 49 |
'data-title', |
| 50 |
'data-tooltip', |
| 51 |
'data-tweak', |
| 52 |
'data-type', |
| 53 |
'data-url', |
| 54 |
'data-video-url', |
| 55 |
'data-whichpage', |
| 56 |
'data-wpo-lcp', |
| 57 |
); |
| 58 |
|
| 59 |
/** |
| 60 |
* Attribute names that are never permitted through this class's filter, regardless of what 'wpo_kses_allowed_attrs' returns. |
| 61 |
* These grant script execution or resource injection independent of the value assigned to them. |
| 62 |
* |
| 63 |
* @var array<string> |
| 64 |
*/ |
| 65 |
private static $blocked_attrs = array( |
| 66 |
'style', |
| 67 |
'srcdoc', |
| 68 |
'formaction', |
| 69 |
); |
| 70 |
|
| 71 |
/** |
| 72 |
* Constructor. |
| 73 |
*/ |
| 74 |
private function __construct() { |
| 75 |
add_filter('wp_kses_allowed_html', array($this, 'wp_kses_allowed_html'), 10, 2); |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* Returns singleton instance. |
| 80 |
* |
| 81 |
* @return WPO_WP_Kses_Compatibility |
| 82 |
*/ |
| 83 |
public static function instance() { |
| 84 |
static $_instance = null; |
| 85 |
if (null === $_instance) { |
| 86 |
$_instance = new self(); |
| 87 |
} |
| 88 |
return $_instance; |
| 89 |
} |
| 90 |
|
| 91 |
/** |
| 92 |
* Add extra attributes to allowed HTML tags for wp_kses, for tags that are already permitted in the given context. |
| 93 |
* |
| 94 |
* @param array<string, array<string, bool>> $allowed_tags Allowed HTML tags and attributes. |
| 95 |
* @param string $context Context for which the allowed tags are being filtered. |
| 96 |
* |
| 97 |
* @return array<string, array<string, bool>> Modified allowed HTML tags and attributes. |
| 98 |
*/ |
| 99 |
public function wp_kses_allowed_html($allowed_tags, $context) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $context is required by the filter but not used in this function |
| 100 |
$attrs = $this->sanitize_allowed_attrs( |
| 101 |
apply_filters('wpo_kses_allowed_attrs', self::$allowed_attrs) |
| 102 |
); |
| 103 |
|
| 104 |
if (!is_array($allowed_tags)) { |
| 105 |
return $allowed_tags; |
| 106 |
} |
| 107 |
|
| 108 |
foreach (array_keys($allowed_tags) as $tag) { |
| 109 |
foreach ($attrs as $attr) { |
| 110 |
$allowed_tags[$tag][$attr] = true; |
| 111 |
} |
| 112 |
} |
| 113 |
|
| 114 |
return $allowed_tags; |
| 115 |
} |
| 116 |
|
| 117 |
/** |
| 118 |
* Validate a list of attribute names before they're merged into kses's allowlist. |
| 119 |
* |
| 120 |
* $allowed_attrs is exposed via the 'wpo_kses_allowed_attrs' filter, |
| 121 |
* so third-party code can add to or replace the list before it reaches wp_kses_allowed_html(). |
| 122 |
* Without this check, a hook (malicious or just careless) could inject event-handler |
| 123 |
* attributes or other script-bearing attribute names directly into kses's allowlist, |
| 124 |
* defeating the sanitizer it's meant to support. |
| 125 |
* This rejects malformed names, event handlers (on*), and a small denylist of other high-risk attribute names |
| 126 |
* |
| 127 |
* @param mixed $attrs Attribute list, expected to be an array of strings. |
| 128 |
* |
| 129 |
* @return array<string> Filtered list of attribute names safe to add to kses's allowlist. |
| 130 |
*/ |
| 131 |
private function sanitize_allowed_attrs($attrs) { |
| 132 |
if (!is_array($attrs)) { |
| 133 |
return self::$allowed_attrs; |
| 134 |
} |
| 135 |
|
| 136 |
/** @var array<string> $sanitized */ |
| 137 |
$sanitized = array(); |
| 138 |
|
| 139 |
foreach ($attrs as $attr) { |
| 140 |
if (!is_string($attr)) { |
| 141 |
continue; |
| 142 |
} |
| 143 |
|
| 144 |
if (!preg_match('/^[a-z][a-z0-9_-]*$/', $attr)) { |
| 145 |
continue; |
| 146 |
} |
| 147 |
|
| 148 |
if (0 === stripos($attr, 'on')) { |
| 149 |
continue; |
| 150 |
} |
| 151 |
|
| 152 |
if (in_array(strtolower($attr), self::$blocked_attrs, true)) { |
| 153 |
continue; |
| 154 |
} |
| 155 |
|
| 156 |
$sanitized[] = $attr; |
| 157 |
} |
| 158 |
|
| 159 |
return $sanitized; |
| 160 |
} |
| 161 |
} |
| 162 |
|
| 163 |
endif; |
| 164 |
|