PluginProbe
JetFormBuilder — Dynamic Blocks Form Builder / 3.6.6
JetFormBuilder — Dynamic Blocks Form Builder v3.6.6
3.6.6.1 3.6.6 3.6.5.4 3.6.5.3 3.6.5.2 3.6.5.1 3.6.5 3.6.4.2 3.6.4.1 3.6.4 3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 All 134 releases
jetformbuilder / includes / form-messages / message-content-processor.php

message-content-processor.php in JetFormBuilder — Dynamic Blocks Form Builder 3.6.6, at includes/form-messages/message-content-processor.php

268 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace Jet_Form_Builder\Form_Messages;
5
6 use Jet_Form_Builder\Classes\Tools;
7 use JFB_Modules\Rich_Content\Macros_Parser;
8 use JFB_Modules\Rich_Content\Module as Rich_Content_Module;
9
10 // If this file is called directly, abort.
11 if ( ! defined( 'WPINC' ) ) {
12 die;
13 }
14
15 class Message_Content_Processor {
16
17 /**
18 * @var Rich_Content_Module
19 */
20 private $rich_content;
21
22 /**
23 * @param Rich_Content_Module|null $rich_content
24 */
25 public function __construct( $rich_content = null ) {
26 $this->rich_content = $rich_content instanceof Rich_Content_Module
27 ? $rich_content
28 : new Rich_Content_Module();
29 }
30
31 /**
32 * Process a message template loaded from saved form/action settings.
33 *
34 * Runtime values are always treated as data. Only active syntax present in the
35 * original template is interpreted, and preset/shortcode modes are mutually
36 * exclusive to prevent cross-parser injection.
37 */
38 public function prepare_trusted( string $message, array $replacements = array() ): string {
39 if ( $this->is_dynamic_preset_template( $message ) ) {
40 $content = $this->rich_content->apply_dynamic_preset( $message );
41 } else {
42 $runtime_values = array();
43 $content = $this->tokenize_macros( $message, $replacements, $runtime_values );
44 $content = $this->apply_shortcodes( $content, $runtime_values );
45 $content = $this->restore_runtime_values( $content, $runtime_values );
46 }
47
48 return $this->finalize( $content );
49 }
50
51 /**
52 * Process a dynamic or unknown status body.
53 *
54 * The body may originate in the request or an external action, so it must not
55 * enter any active parser.
56 */
57 public function prepare_untrusted( string $message ): string {
58 return $this->finalize( $message );
59 }
60
61 public function prepare_macros( string $message, array $replacements = array() ): string {
62 $runtime_values = array();
63 $content = $this->tokenize_macros( $message, $replacements, $runtime_values );
64
65 return $this->restore_runtime_values( $content, $runtime_values );
66 }
67
68 /**
69 * Replace every runtime macro with a unique token before shortcode parsing.
70 *
71 * A token is bound to its value only after WordPress has fixed the shortcode
72 * tag and attribute structure. This prevents values from completing a tag name
73 * or escaping a quoted attribute while preserving macros used as attribute data.
74 */
75 private function tokenize_macros(
76 string $message,
77 array $replacements,
78 array &$runtime_values
79 ): string {
80 $parser = new Macros_Parser();
81 $content = preg_replace_callback(
82 '/%([\w\-].*?\S?)%/',
83 function ( array $match ) use ( $parser, $replacements, $message, &$runtime_values ) {
84 $token = $this->generate_runtime_token( $message, $runtime_values );
85 $value = $parser->parse_macros( $match[0], $replacements );
86
87 $runtime_values[ $token ] = $this->neutralize_shortcodes( $value );
88
89 return $token;
90 },
91 $message
92 );
93
94 return is_string( $content ) ? $content : '';
95 }
96
97 private function generate_runtime_token( string $message, array $runtime_values ): string {
98 do {
99 $token = 'jfbmacro' . str_replace( '-', '', wp_generate_uuid4() );
100 } while ( false !== strpos( $message, $token ) || isset( $runtime_values[ $token ] ) );
101
102 return $token;
103 }
104
105 /**
106 * Execute shortcodes while runtime values remain outside shortcode grammar.
107 *
108 * WordPress has already parsed the tag and its attributes when this late
109 * pre_do_shortcode_tag callback runs. The callback therefore receives restored
110 * values as data, not as source text that can add tags or attributes.
111 */
112 private function apply_shortcodes( string $content, array $runtime_values ): string {
113 if ( ! $runtime_values ) {
114 return $this->rich_content->apply_shortcodes( $content );
115 }
116
117 $interceptor = function ( $return, $tag, $attributes, $match ) use ( $runtime_values ) {
118 if ( false !== $return || ! $this->contains_runtime_token( $match, $runtime_values ) ) {
119 return $return;
120 }
121
122 global $shortcode_tags;
123
124 if ( empty( $shortcode_tags[ $tag ] ) || ! is_callable( $shortcode_tags[ $tag ] ) ) {
125 return $return;
126 }
127
128 $resolved_attributes = $this->restore_callback_values( $attributes, $runtime_values );
129 $content = $match[5] ?? null;
130 $resolved_content = is_null( $content )
131 ? null
132 : $this->restore_callback_values( $content, $runtime_values );
133 $output = $match[1] . call_user_func(
134 $shortcode_tags[ $tag ],
135 $resolved_attributes,
136 $resolved_content,
137 $tag
138 ) . $match[6];
139
140 // Core skips do_shortcode_tag after a pre_do_shortcode_tag short circuit.
141 return apply_filters(
142 'do_shortcode_tag',
143 $output,
144 $tag,
145 $resolved_attributes,
146 $match
147 );
148 };
149
150 add_filter( 'pre_do_shortcode_tag', $interceptor, PHP_INT_MAX, 4 );
151
152 try {
153 return $this->rich_content->apply_shortcodes( $content );
154 } finally {
155 remove_filter( 'pre_do_shortcode_tag', $interceptor, PHP_INT_MAX );
156 }
157 }
158
159 private function contains_runtime_token( $value, array $runtime_values ): bool {
160 if ( is_array( $value ) ) {
161 foreach ( $value as $item ) {
162 if ( $this->contains_runtime_token( $item, $runtime_values ) ) {
163 return true;
164 }
165 }
166
167 return false;
168 }
169
170 if ( ! is_string( $value ) ) {
171 return false;
172 }
173
174 foreach ( $runtime_values as $token => $runtime_value ) {
175 if ( false !== strpos( $value, $token ) ) {
176 return true;
177 }
178 }
179
180 return false;
181 }
182
183 /**
184 * Restore tokens only in parsed values, never in shortcode tag/attribute names.
185 *
186 * If restoration would assemble another registered shortcode inside a value,
187 * keep that value tokenized until the outer shortcode has finished. It is then
188 * restored as text and neutralized by finalize().
189 */
190 private function restore_callback_values( $value, array $runtime_values ) {
191 if ( is_array( $value ) ) {
192 foreach ( $value as $key => $item ) {
193 $value[ $key ] = $this->restore_callback_values( $item, $runtime_values );
194 }
195
196 return $value;
197 }
198
199 if ( ! is_string( $value ) ) {
200 return $value;
201 }
202
203 $resolved = $this->restore_runtime_values( $value, $runtime_values );
204
205 return $this->introduces_registered_shortcode( $value, $resolved ) ? $value : $resolved;
206 }
207
208 private function restore_runtime_values( string $content, array $runtime_values ): string {
209 return $runtime_values ? strtr( $content, $runtime_values ) : $content;
210 }
211
212 private function introduces_registered_shortcode( string $before, string $after ): bool {
213 $before_tags = $this->get_registered_shortcode_counts( $before );
214 $after_tags = $this->get_registered_shortcode_counts( $after );
215
216 foreach ( $after_tags as $tag => $count ) {
217 if ( $count > ( $before_tags[ $tag ] ?? 0 ) ) {
218 return true;
219 }
220 }
221
222 return false;
223 }
224
225 private function get_registered_shortcode_counts( string $content ): array {
226 global $shortcode_tags;
227
228 if ( false === strpos( $content, '[' ) || empty( $shortcode_tags ) ) {
229 return array();
230 }
231
232 preg_match_all(
233 '@\[([^<>&/\[\]\x00-\x20=]++)@',
234 $content,
235 $matches
236 );
237
238 $tags = array_intersect( array_keys( $shortcode_tags ), $matches[1] ?? array() );
239
240 return array_count_values( $tags );
241 }
242
243 private function is_dynamic_preset_template( string $message ): bool {
244 $decoded = json_decode( $message, true );
245
246 return is_array( $decoded ) && ! empty( $decoded['jet_preset'] );
247 }
248
249 /**
250 * Keep literal brackets visible while preventing this value from introducing
251 * shortcode syntax into a trusted template.
252 */
253 private function neutralize_shortcodes( string $content ): string {
254 return str_replace(
255 array( '[', ']' ),
256 array( '&#91;', '&#93;' ),
257 $content
258 );
259 }
260
261 private function finalize( $content ): string {
262 $content = Tools::to_string( $content );
263 $content = wp_kses_post( $content );
264
265 return $this->neutralize_shortcodes( $content );
266 }
267 }
268