PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.01
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.01
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | app/Services/SmartCodeParser.php +93 -12 2.4.012.10.01 View file →
@@ -51,18 +51,39 @@
51 51 }
52 52
53 53 public function parseShortcode($string)
54 54 {
55 - // check if the string contains any smartcode
56 55 if (strpos($string, '{{') === false && strpos($string, '##') === false) {
57 56 return $string;
58 57 }
59 58
59 + if (static::$isHtml) {
60 + $string = $this->resolveHrefPlaceholders($string);
61 + }
62 +
60 63 return preg_replace_callback('/({{|##)+(.*?)(}}|##)/', function ($matches) {
61 64 return $this->replace($matches);
62 65 }, $string);
63 66 }
64 67
68 + /**
69 + * Resolve placeholders inside href="..." attributes to raw URLs.
70 + * Prevents nested anchors when URL smartcodes auto-wrap in HTML mode.
71 + */
72 + protected function resolveHrefPlaceholders($string)
73 + {
74 + return preg_replace_callback(
75 + '/(?<![a-zA-Z-])href\s*=\s*(["\'])([^"\']*(?:\{\{|##)[^"\']*)\1/i',
76 + function ($m) {
77 + static::$isHtml = false;
78 + $resolved = $this->parseShortcode($m[2]);
79 + static::$isHtml = true;
80 + return 'href=' . $m[1] . esc_url($resolved) . $m[1];
81 + },
82 + $string
83 + );
84 + }
85 +
65 86 protected function replace($matches)
66 87 {
67 88 if (empty($matches[2])) {
68 89 return apply_filters('fluent_community/smartcode_fallback', $matches[0], $this->store['user']);
@@ -122,39 +143,75 @@
122 143
123 144 if ($transformer && is_string($transformer) && $value) {
124 145 switch ($transformer) {
125 146 case 'trim':
126 - return trim($value);
147 + $value = trim($value);
148 + break;
127 149 case 'ucfirst':
128 - return ucfirst($value);
150 + $value = ucfirst($value);
151 + break;
129 152 case 'strtolower':
130 - return strtolower($value);
153 + $value = strtolower($value);
154 + break;
131 155 case 'strtoupper':
132 - return strtoupper($value);
156 + $value = strtoupper($value);
157 + break;
133 158 case 'ucwords':
134 - return ucwords($value);
159 + $value = ucwords($value);
160 + break;
135 161 case 'concat_first': // usage: {{contact.first_name||concat_first|Hi
136 162 if (isset($valueKeys[3])) {
137 163 $value = trim($valueKeys[3] . ' ' . $value);
138 164 }
139 - return $value;
165 + break;
140 166 case 'concat_last': // usage: {{contact.first_name||concat_last|, => FIRST_NAME,
141 167 if (isset($valueKeys[3])) {
142 168 $value = trim($value . '' . $valueKeys[3]);
143 169 }
144 - return $value;
170 + break;
145 171 case 'show_if': // usage {{contact.first_name||show_if|First name exist
146 172 if (isset($valueKeys[3])) {
147 173 $value = $valueKeys[3];
148 174 }
149 - return $value;
150 - default:
151 - return $value;
175 + break;
152 176 }
153 177 }
154 178
155 - return $value;
179 + return $this->escapeValueForContext($value, $dataKey, $valueKey);
180 + }
156 181
182 + /**
183 + * Smartcode values are substituted into lockscreen/lesson/email HTML *after*
184 + * that content has passed through wp_kses / do_blocks, so a resolved scalar
185 + * carrying markup would otherwise bypass sanitisation. Escape ordinary
186 + * values for the HTML context. The few branches that intentionally build a
187 + * trusted fragment (photo_html, name_with_url, section url) already escape
188 + * their own interpolated parts, so they are left untouched, and values from
189 + * third-party group callbacks are the extension's responsibility.
190 + */
191 + protected function escapeValueForContext($value, $dataKey, $valueKey)
192 + {
193 + if (!static::$isHtml || !is_string($value) || $value === '') {
194 + return $value;
195 + }
196 +
197 + $knownGroups = ['site', 'user', 'community', 'section', 'course'];
198 + if (!in_array($dataKey, $knownGroups, true)) {
199 + return $value;
200 + }
201 +
202 + $trustedHtml = [
203 + 'user' => ['photo_html'],
204 + 'community' => ['name_with_url'],
205 + 'section' => ['url'],
206 + ];
207 +
208 + $baseKey = strtok($valueKey, '.'); // "photo_html.50px" -> "photo_html"
209 + if (in_array($baseKey, Arr::get($trustedHtml, $dataKey, []), true)) {
210 + return $value;
211 + }
212 +
213 + return esc_html($value);
157 214 }
158 215
159 216 protected function getWpValue($valueKey, $defaultValue)
160 217 {
@@ -205,8 +262,22 @@
205 262
206 263 $wpUser = $userModel->getWpUser();
207 264 $valueKeys = explode('.', $valueKey);
208 265 if (count($valueKeys) == 1) {
266 + // Smartcodes are resolved against the *viewer* and can be authored by
267 + // space/course/page admins, so only a fixed set of non-sensitive
268 + // profile fields may be read. Never expose user_pass,
269 + // user_activation_key, session tokens or capability meta.
270 + $allowedFields = apply_filters('fluent_community/smartcode/user_fields', [
271 + 'ID', 'first_name', 'last_name', 'nickname', 'display_name',
272 + 'user_email', 'user_login', 'user_nicename', 'user_url',
273 + 'description', 'user_registered'
274 + ]);
275 +
276 + if (!in_array($valueKey, $allowedFields, true)) {
277 + return $defaultValue;
278 + }
279 +
209 280 $value = $wpUser->get($valueKey);
210 281 if (!$value) {
211 282 return $defaultValue;
212 283 }
@@ -230,8 +301,18 @@
230 301 return '<img ' . $style . ' src="' . esc_url($userModel->photo) . '" alt="' . esc_attr($userModel->display_name) . '" class="fcom_user_dynamic_photo" />';
231 302 }
232 303
233 304 if ($customKey == 'meta') {
305 + // No first-party template reads user meta through smartcodes, and the
306 + // viewer's own meta (session_tokens, capability keys, reset keys,
307 + // any _-prefixed value) must never leak into author-controlled
308 + // content. Resolve only meta keys a site has explicitly allowed.
309 + $allowedMetaKeys = apply_filters('fluent_community/smartcode/user_meta_keys', []);
310 +
311 + if (strpos($customProperty, '_') === 0 || !in_array($customProperty, $allowedMetaKeys, true)) {
312 + return $defaultValue;
313 + }
314 +
234 315 $metaValue = get_user_meta($wpUser->ID, $customProperty, true);
235 316 if (!$metaValue) {
236 317 return $defaultValue;
237 318 }