PluginProbe
Autoptimize / 3.1.16
Autoptimize v3.1.16
3.1.16 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 All 108 releases
autoptimize / classes / autoptimizeAttributeParser.php

autoptimizeAttributeParser.php in Autoptimize 3.1.16, at classes/autoptimizeAttributeParser.php

259 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class autoptimizeAttributeParser
8 {
9 /**
10 * Protocols allowed during attribute parsing.
11 * Extends WP's default list with protocols that legitimately appear in HTML
12 * attributes but would otherwise be stripped by wp_kses_hair.
13 */
14 private static function allowed_protocols(): array {
15 return array_merge( wp_allowed_protocols(), [ 'data', 'blob' ] );
16 }
17
18 /**
19 * Entry point for modifying HTML attributes.
20 */
21 public static function modify( $tag, $target, $swaps = [], $replacements = []): string
22 {
23 $pieces = wp_html_split( $tag );
24
25 foreach ( $pieces as &$piece ) {
26 // Skip empty strings, text nodes, and comments.
27 if ( $piece === '' || $piece[0] !== '<' || $piece[1] === '!' ) {
28 continue;
29 }
30
31 // Skip closing tags.
32 if ( $piece[1] === '/' ) {
33 continue;
34 }
35
36 // Extract tag name; skip if not parseable.
37 if ( ! preg_match( '/^<([a-z0-9]+)/i', $piece, $tag_name_match ) ) {
38 continue;
39 }
40
41 // Skip non-matching tags.
42 if ( strcasecmp( $tag_name_match[1], $target ) !== 0 ) {
43 continue;
44 }
45
46 // This piece matches $target — apply modifications.
47 $attr_string = preg_replace( '/^<[a-z0-9]+\s*|(\s?\/?>)$/i', '', $piece );
48 $attrs = wp_kses_hair( $attr_string, self::allowed_protocols() );
49
50 // 1. Handle Deletions first to clear the deck
51 foreach ( $replacements as $attr_name => $new_value ) {
52 if ( is_null( $new_value ) ) {
53 unset( $attrs[$attr_name] );
54 }
55 }
56
57 // 2. Handle Swaps
58 foreach ( $swaps as $old => $new ) {
59 if ( isset( $attrs[$old] ) ) {
60 $attrs[$new] = $attrs[$old];
61 $attrs[$new]['name'] = $new;
62 unset( $attrs[$old] );
63 }
64 }
65
66 // 3. Handle Replacements & Additions
67 foreach ( $replacements as $attr_name => $new_value ) {
68 if ( ! is_null( $new_value ) ) {
69 $attrs[$attr_name] = [
70 'name' => $attr_name,
71 'value' => $new_value,
72 ];
73 }
74 }
75
76 // Rebuild this tag.
77 $rebuilt = '<' . $tag_name_match[1];
78 foreach ( $attrs as $name => $data ) {
79 $value = esc_attr( $data['value'] );
80 $rebuilt .= " {$name}=\"{$value}\"";
81 }
82 $rebuilt .= ( strpos( $piece, '/>' ) !== false ) ? ' />' : '>';
83
84 $piece = $rebuilt;
85 }
86 unset( $piece );
87
88 return implode( '', $pieces );
89 }
90
91 /**
92 * Entry point for retrieving attributes.
93 * Returns a single value (string|null) if a string name is provided,
94 * or an associative array if a regex is provided.
95 */
96 public static function get( $tag, $target, $query ) // : string|array|null TODO: uncomment when PHP7.4 is no longer supported
97 {
98 $pieces = wp_html_split( $tag );
99
100 foreach ( $pieces as $piece ) {
101 // Skip empty strings, text nodes, and comments.
102 if ( $piece === '' || $piece[0] !== '<' || $piece[1] === '!' ) {
103 continue;
104 }
105
106 // Skip closing tags.
107 if ( $piece[1] === '/' ) {
108 continue;
109 }
110
111 // Extract tag name.
112 if ( ! preg_match( '/^<([a-z0-9]+)/i', $piece, $tag_name_match ) ) {
113 continue;
114 }
115
116 // Skip non-matching tags.
117 if ( strcasecmp( $tag_name_match[1], $target ) !== 0 ) {
118 continue;
119 }
120
121 // Found a matching tag — parse its attributes.
122 $attr_string = preg_replace( '/^<[a-z0-9]+\s*|(\s?\/?>)$/i', '', $piece );
123 $attrs = wp_kses_hair( $attr_string, self::allowed_protocols() );
124
125 // Single attribute request
126 if ( is_string( $query ) && strpos( $query, '/' ) !== 0 ) {
127 return $attrs[$query]['value'] ?? null;
128 }
129
130 // Regex request
131 $results = [];
132 foreach ( $attrs as $name => $data ) {
133 if ( preg_match( $query, $name ) ) {
134 $results[$name] = $data['value'];
135 }
136 }
137 return $results;
138 }
139
140 // No matching tag found.
141 return is_string( $query ) && strpos( $query, '/' ) !== 0 ? null : [];
142 }
143
144 /**
145 * Helper to rename attributes.
146 * Supports:
147 * - rename( $tag, 'src', 'data-src' )
148 * - rename( $tag, ['src' => 'data-src', 'srcset' => 'data-srcset'] )
149 */
150 public static function rename( $tag, $target, $old_name, $new_name = null ): string {
151 if ( is_array( $old_name ) ) {
152 $swaps = $old_name;
153 } else {
154 $swaps = [ $old_name => $new_name ];
155 }
156
157 return self::modify( $tag, $target, $swaps );
158 }
159
160 /**
161 * Helper to remove one or more attributes.
162 * Supports:
163 * - remove( $tag, 'style' )
164 * - remove( $tag, ['style', 'onclick', 'onerror'] )
165 */
166 public static function remove( $tag, $target, $attributes ): string {
167 // array_fill_keys expects an array; (array) cast handles single strings safely.
168 $replacements = array_fill_keys( (array) $attributes, null );
169
170 return self::modify( $tag, $target, [], $replacements );
171 }
172
173 /**
174 * Helper to replace or add attribute values.
175 * Supports:
176 * - replace( $tag, 'class', 'lazyload' )
177 * - replace( $tag, ['class' => 'lazyload', 'src' => 'placeholder.png'] )
178 */
179 public static function replace( $tag, $target, $attribute, $value = null ): string {
180 if ( is_array( $attribute ) ) {
181 $replacements = $attribute;
182 } else {
183 $replacements = [ $attribute => $value ];
184 }
185
186 return self::modify( $tag, $target, [], $replacements );
187 }
188
189 /**
190 * Helper to rebuild a HTML tag from a string
191 * Can be used to fix minified HTML that is missing quotes around attributes
192 * Supports:
193 * - replace( $tag )
194 */
195 public static function rebuild( $tag, $target ): string
196 {
197 return self::modify( $tag, $target, [], [], true );
198 }
199
200 /**
201 * Retrieves a single attribute value.
202 * @param string $tag The HTML tag string.
203 * @param string $name The attribute name to find.
204 * @return string|null The attribute value or null if not found.
205 */
206 public static function get_attribute( string $tag, string $target, string $name ): ?string
207 {
208 $result = self::get( $tag, $target, $name );
209 return is_string( $result ) ? $result : null;
210 }
211
212 /**
213 * Retrieves a list of attributes matching a regex or prefix.
214 * @param string $tag The HTML tag string.
215 * @param string $regex The regex pattern to match keys against (e.g., '/^data-/').
216 * @return array Associative array of [name => value].
217 */
218 public static function get_attributes( string $tag, string $target, string $regex ): array
219 {
220 $result = self::get( $tag, $target, $regex );
221 return is_array( $result ) ? $result : [];
222 }
223
224 /**
225 * Modifies a single specific attribute via callback.
226 * If callback returns null, the attribute is removed.
227 */
228 public static function modify_attribute( string $tag, string $target, string $name, callable $callback ): string
229 {
230 $value = self::get_attribute( $tag, $target, $name );
231 $new_value = $callback( $name, $value );
232
233 if ( $new_value === $value ) {
234 return $tag;
235 }
236
237 return self::modify( $tag, $target, [], [ $name => $new_value ] );
238 }
239
240 /**
241 * Modifies multiple attributes matching a regex via callback.
242 */
243 public static function modify_attributes( string $tag, string $target, string $regex, callable $callback ): string
244 {
245 $attrs = self::get_attributes( $tag, $target, $regex );
246 $replacements = [];
247
248 foreach ( $attrs as $name => $value ) {
249 $new_value = $callback( $name, $value );
250
251 if ( $new_value !== $value ) {
252 $replacements[ $name ] = $new_value;
253 }
254 }
255
256 return empty( $replacements ) ? $tag : self::modify( $tag, $target, [], $replacements );
257 }
258 }
259