Analytics
2 weeks ago
Database
2 months ago
DynamicFieldResolver.php
1 month ago
Elementor_Enhancer.php
6 months ago
EmbedPress_Core_Installer.php
6 years ago
EmbedPress_Notice.php
3 months ago
EmbedPress_Plugin_Usage_Tracker.php
1 month ago
Extend_CustomPlayer_Controls.php
2 months ago
Extend_Elementor_Controls.php
1 year ago
FeatureNoticeManager.php
4 days ago
FeatureNotices.php
4 days ago
FeaturePreviewModal.php
4 days ago
Feature_Enhancer.php
1 month ago
GoogleReviewsAdminPage.php
4 days ago
GoogleReviewsApify.php
4 days ago
GoogleReviewsManaged.php
4 days ago
GoogleReviewsRenderer.php
4 days ago
GoogleReviewsRestController.php
4 days ago
GoogleReviewsStore.php
4 days ago
Helper.php
4 days ago
Pdf_Thumbnail_Handler.php
2 months ago
PermalinkHelper.php
10 months ago
View_Count_Display.php
2 weeks ago
DynamicFieldResolver.php
375 lines
| 1 | <?php |
| 2 | |
| 3 | namespace EmbedPress\Includes\Classes; |
| 4 | |
| 5 | if (!defined('ABSPATH')) { |
| 6 | exit; |
| 7 | } |
| 8 | |
| 9 | /** |
| 10 | * Resolves dynamic embed URLs from custom-field sources (ACF, MetaBox, Pods, |
| 11 | * Toolset, JetEngine, raw post meta). |
| 12 | * |
| 13 | * Used by: |
| 14 | * - Elementor PDF / Document widgets via resolve_elementor_dynamic() |
| 15 | * - Gutenberg block renderer + shortcode via resolve_field() |
| 16 | * |
| 17 | * Why one class: the three surfaces (Elementor / Gutenberg / shortcode) all |
| 18 | * need the same source map. Keeping it in one place means adding a new field |
| 19 | * plugin (e.g. SCF, Carbon Fields) touches one method, not three. |
| 20 | */ |
| 21 | class DynamicFieldResolver |
| 22 | { |
| 23 | /** |
| 24 | * Resolve a custom-field URL given an explicit source + field name. |
| 25 | * Used by Gutenberg blocks and shortcodes (which know their source directly). |
| 26 | * |
| 27 | * @param string $source acf|metabox|pods|toolset|jetengine|meta |
| 28 | * @param string $field Field name / key on the current post. |
| 29 | * @param int|null $post_id Defaults to current queried post. |
| 30 | * @return string Resolved URL, or '' when no value / source unavailable. |
| 31 | */ |
| 32 | public static function resolve_field($source, $field, $post_id = null) |
| 33 | { |
| 34 | // Field/meta keys are case-sensitive (MetaBox & ACF custom IDs and many |
| 35 | // hand-registered keys use uppercase). sanitize_key() would lowercase |
| 36 | // them and the lookup would miss, so only strip HTML/whitespace while |
| 37 | // preserving case, and reject obviously unsafe characters. |
| 38 | $field = trim(wp_strip_all_tags((string) $field)); |
| 39 | if ($field === '' || !preg_match('/^[A-Za-z0-9_\-]+$/', $field)) { |
| 40 | return ''; |
| 41 | } |
| 42 | |
| 43 | if ($post_id === null) { |
| 44 | $post_id = get_the_ID(); |
| 45 | } |
| 46 | if (!$post_id) { |
| 47 | return ''; |
| 48 | } |
| 49 | |
| 50 | $url = ''; |
| 51 | |
| 52 | switch ($source) { |
| 53 | case 'acf': |
| 54 | if (function_exists('get_field')) { |
| 55 | $url = self::extract_url(get_field($field, $post_id)); |
| 56 | } |
| 57 | break; |
| 58 | |
| 59 | case 'metabox': |
| 60 | // MetaBox exposes rwmb_meta(); fall back to raw post meta if unavailable. |
| 61 | if (function_exists('rwmb_meta')) { |
| 62 | $url = self::extract_url(rwmb_meta($field, '', $post_id)); |
| 63 | } else { |
| 64 | $url = self::extract_url(get_post_meta($post_id, $field, true)); |
| 65 | } |
| 66 | break; |
| 67 | |
| 68 | case 'pods': |
| 69 | if (function_exists('pods_field')) { |
| 70 | $url = self::extract_url(pods_field(get_post_type($post_id), $post_id, $field, true)); |
| 71 | } else { |
| 72 | $url = self::extract_url(get_post_meta($post_id, $field, true)); |
| 73 | } |
| 74 | break; |
| 75 | |
| 76 | case 'toolset': |
| 77 | // Toolset Types prefixes meta keys with `wpcf-`. |
| 78 | $url = self::extract_url(get_post_meta($post_id, 'wpcf-' . $field, true)); |
| 79 | break; |
| 80 | |
| 81 | case 'jetengine': |
| 82 | case 'meta': |
| 83 | default: |
| 84 | $url = self::extract_url(get_post_meta($post_id, $field, true)); |
| 85 | break; |
| 86 | } |
| 87 | |
| 88 | // Back-compat: existing free 4.5.x filter, applied by the legacy |
| 89 | // Elementor PDF/Document inline resolvers. Keep firing it from the |
| 90 | // central path so third-party code keeps working. |
| 91 | $url = apply_filters('embedpress/custom_meta_field_value', $url, $field); |
| 92 | |
| 93 | return is_string($url) ? trim($url) : ''; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Coerce a custom-field value of any shape into a single URL string. |
| 98 | * |
| 99 | * Field plugins return file/image values in many shapes: |
| 100 | * - plain URL string → used as-is |
| 101 | * - attachment ID (int or numeric string) → wp_get_attachment_url() |
| 102 | * - associative array (ACF/MetaBox/Pods) → url | full_url | guid | src |
| 103 | * - nested/indexed array of the above |
| 104 | * (MetaBox file_advanced, multi-value) → first resolvable entry |
| 105 | * |
| 106 | * @param mixed $value Raw field value. |
| 107 | * @return string Resolved URL, or '' if nothing usable. |
| 108 | */ |
| 109 | private static function extract_url($value) |
| 110 | { |
| 111 | if (is_string($value)) { |
| 112 | $value = trim($value); |
| 113 | if ($value === '') { |
| 114 | return ''; |
| 115 | } |
| 116 | // A bare numeric string is an attachment ID, not a URL. |
| 117 | if (ctype_digit($value)) { |
| 118 | $resolved = wp_get_attachment_url((int) $value); |
| 119 | return $resolved ? $resolved : ''; |
| 120 | } |
| 121 | return $value; |
| 122 | } |
| 123 | |
| 124 | if (is_int($value)) { |
| 125 | $resolved = wp_get_attachment_url($value); |
| 126 | return $resolved ? $resolved : ''; |
| 127 | } |
| 128 | |
| 129 | if (is_array($value)) { |
| 130 | // Direct URL-bearing keys, in priority order. |
| 131 | foreach (['url', 'full_url', 'guid', 'src'] as $key) { |
| 132 | if (!empty($value[$key]) && is_string($value[$key])) { |
| 133 | return trim($value[$key]); |
| 134 | } |
| 135 | } |
| 136 | // An attachment ID nested under common keys. |
| 137 | foreach (['ID', 'id'] as $key) { |
| 138 | if (!empty($value[$key]) && (is_int($value[$key]) || ctype_digit((string) $value[$key]))) { |
| 139 | $resolved = wp_get_attachment_url((int) $value[$key]); |
| 140 | if ($resolved) { |
| 141 | return $resolved; |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | // Indexed / keyed collection (e.g. MetaBox file_advanced) — return |
| 146 | // the first entry that resolves. |
| 147 | foreach ($value as $item) { |
| 148 | $resolved = self::extract_url($item); |
| 149 | if ($resolved !== '') { |
| 150 | return $resolved; |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | return ''; |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * Resolve a dynamic URL coming from Elementor's `__dynamic__` payload |
| 160 | * (the picker emits an HTML-encoded blob containing the source + field). |
| 161 | * |
| 162 | * @param string $dynamic_value Raw `__dynamic__[<control>]` value. |
| 163 | * @return string Resolved URL, or '' if nothing could be resolved. |
| 164 | */ |
| 165 | public static function resolve_elementor_dynamic($dynamic_value) |
| 166 | { |
| 167 | if (empty($dynamic_value)) { |
| 168 | return ''; |
| 169 | } |
| 170 | |
| 171 | $decoded = urldecode($dynamic_value); |
| 172 | |
| 173 | if (!preg_match('/name="([^"]+)"/', $decoded, $name_matches)) { |
| 174 | return ''; |
| 175 | } |
| 176 | $name_key = $name_matches[1]; |
| 177 | |
| 178 | $source = ''; |
| 179 | $pattern = ''; |
| 180 | |
| 181 | if ($name_key === 'acf-url' && class_exists('ACF') && function_exists('get_field')) { |
| 182 | $source = 'acf'; |
| 183 | $pattern = '/"key":"[^"]+:(.*?)"/'; |
| 184 | } elseif ($name_key === 'toolset-url' && class_exists('Types_Helper_Output_Meta_Box')) { |
| 185 | $source = 'toolset'; |
| 186 | $pattern = '/"key":"[^"]+:(.*?)"/'; |
| 187 | } elseif ($name_key === 'jet-post-custom-field' && class_exists('Jet_Engine')) { |
| 188 | $source = 'jetengine'; |
| 189 | $pattern = '/"meta_field":"([^"]+)"/'; |
| 190 | } elseif ($name_key === 'post-custom-field') { |
| 191 | // Elementor Pro's native "Post Custom Field" tag — URL_CATEGORY, |
| 192 | // so it can target the EmbedPress PDF/Document URL control. Maps |
| 193 | // to raw post_meta via get_post_meta(). Settings emit either |
| 194 | // `key` (predefined dropdown of meta keys) or `custom_key` |
| 195 | // (free-form input); try `custom_key` first because when both are |
| 196 | // present Elementor's own render() prefers `key`, but `key` may |
| 197 | // be empty and `custom_key` carries the actual field name. |
| 198 | $source = 'meta'; |
| 199 | $pattern = '/"custom_key":"([^"]+)"/'; |
| 200 | if (!preg_match($pattern, $decoded, $m) || empty($m[1])) { |
| 201 | $pattern = '/"key":"([^"]+)"/'; |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | if ($source === '' || !preg_match($pattern, $decoded, $matches) || empty($matches[1])) { |
| 206 | return self::elementor_fallback($decoded); |
| 207 | } |
| 208 | |
| 209 | $url = self::resolve_field($source, $matches[1]); |
| 210 | |
| 211 | if ($url === '') { |
| 212 | $url = self::elementor_fallback($decoded); |
| 213 | } |
| 214 | |
| 215 | return esc_url_raw($url); |
| 216 | } |
| 217 | |
| 218 | private static function elementor_fallback($decoded) |
| 219 | { |
| 220 | if (preg_match('/"fallback":"([^"]+)"/', $decoded, $m)) { |
| 221 | return (string) $m[1]; |
| 222 | } |
| 223 | return ''; |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * REST: enumerate available custom-field keys for the requested source. |
| 228 | * |
| 229 | * Used by the Gutenberg PDF block's Dynamic Source inspector so editors |
| 230 | * pick a field from a dropdown instead of typing the key by hand. Each |
| 231 | * source uses its own native API (ACF field groups, MetaBox registry, |
| 232 | * Pods fields, JetEngine meta-boxes, Toolset Types definitions); the |
| 233 | * `meta` source falls back to distinct `wp_postmeta` keys excluding the |
| 234 | * usual core/private prefixes. |
| 235 | * |
| 236 | * Response: { fields: [{ value, label }, ...], available: bool, message } |
| 237 | */ |
| 238 | public static function rest_list_fields(\WP_REST_Request $request) |
| 239 | { |
| 240 | $source = sanitize_key((string) $request->get_param('source')); |
| 241 | $fields = []; |
| 242 | $available = true; |
| 243 | $message = ''; |
| 244 | |
| 245 | switch ($source) { |
| 246 | case 'acf': |
| 247 | if (!function_exists('acf_get_field_groups')) { |
| 248 | $available = false; |
| 249 | $message = 'ACF plugin is not active.'; |
| 250 | break; |
| 251 | } |
| 252 | foreach (acf_get_field_groups() as $group) { |
| 253 | foreach (acf_get_fields($group['key']) ?: [] as $field) { |
| 254 | $fields[] = [ |
| 255 | 'value' => $field['name'], |
| 256 | 'label' => $field['label'] . ' (' . $field['name'] . ')', |
| 257 | ]; |
| 258 | } |
| 259 | } |
| 260 | break; |
| 261 | |
| 262 | case 'metabox': |
| 263 | if (!function_exists('rwmb_get_registry')) { |
| 264 | $available = false; |
| 265 | $message = 'Meta Box plugin is not active.'; |
| 266 | break; |
| 267 | } |
| 268 | $registry = rwmb_get_registry('field'); |
| 269 | foreach ($registry->get_by_object_type('post') as $post_type_fields) { |
| 270 | foreach ($post_type_fields as $field) { |
| 271 | if (empty($field['id'])) continue; |
| 272 | $fields[] = [ |
| 273 | 'value' => $field['id'], |
| 274 | 'label' => ($field['name'] ?? $field['id']) . ' (' . $field['id'] . ')', |
| 275 | ]; |
| 276 | } |
| 277 | } |
| 278 | break; |
| 279 | |
| 280 | case 'pods': |
| 281 | if (!function_exists('pods_api')) { |
| 282 | $available = false; |
| 283 | $message = 'Pods plugin is not active.'; |
| 284 | break; |
| 285 | } |
| 286 | $pods = pods_api()->load_pods(['type' => 'post_type', 'names' => true]) ?: []; |
| 287 | foreach ($pods as $pod_slug => $pod_label) { |
| 288 | $pod = pods_api()->load_pod(['name' => $pod_slug]); |
| 289 | foreach ($pod['fields'] ?? [] as $field_name => $field) { |
| 290 | $fields[] = [ |
| 291 | 'value' => $field_name, |
| 292 | 'label' => ($field['label'] ?? $field_name) . ' (' . $field_name . ')', |
| 293 | ]; |
| 294 | } |
| 295 | } |
| 296 | break; |
| 297 | |
| 298 | case 'jetengine': |
| 299 | if (!class_exists('Jet_Engine')) { |
| 300 | $available = false; |
| 301 | $message = 'JetEngine plugin is not active.'; |
| 302 | break; |
| 303 | } |
| 304 | if (!empty(\Jet_Engine::instance()->meta_boxes)) { |
| 305 | foreach (\Jet_Engine::instance()->meta_boxes->data->get_items() as $meta_box) { |
| 306 | foreach ($meta_box['meta_fields'] ?? [] as $field) { |
| 307 | if (empty($field['name'])) continue; |
| 308 | $fields[] = [ |
| 309 | 'value' => $field['name'], |
| 310 | 'label' => ($field['title'] ?? $field['name']) . ' (' . $field['name'] . ')', |
| 311 | ]; |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | break; |
| 316 | |
| 317 | case 'toolset': |
| 318 | if (!function_exists('wpcf_admin_fields_get_fields')) { |
| 319 | $available = false; |
| 320 | $message = 'Toolset Types plugin is not active.'; |
| 321 | break; |
| 322 | } |
| 323 | foreach (wpcf_admin_fields_get_fields() as $slug => $field) { |
| 324 | $fields[] = [ |
| 325 | 'value' => $slug, |
| 326 | 'label' => ($field['name'] ?? $slug) . ' (' . $slug . ')', |
| 327 | ]; |
| 328 | } |
| 329 | break; |
| 330 | |
| 331 | case 'meta': |
| 332 | default: |
| 333 | global $wpdb; |
| 334 | $rows = $wpdb->get_col( |
| 335 | "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} " |
| 336 | . "WHERE meta_key NOT LIKE '\\_%' " |
| 337 | . "AND meta_key NOT LIKE '\\\\_%' " |
| 338 | . "ORDER BY meta_key ASC LIMIT 200" |
| 339 | ); |
| 340 | foreach ($rows as $key) { |
| 341 | $fields[] = ['value' => $key, 'label' => $key]; |
| 342 | } |
| 343 | break; |
| 344 | } |
| 345 | |
| 346 | return rest_ensure_response([ |
| 347 | 'source' => $source, |
| 348 | 'available' => $available, |
| 349 | 'message' => $message, |
| 350 | 'fields' => $fields, |
| 351 | ]); |
| 352 | } |
| 353 | |
| 354 | /** |
| 355 | * REST: resolve a single source+field to its URL for a given post, so the |
| 356 | * block editor can live-preview the dynamic value instead of the saved |
| 357 | * placeholder. Mirrors resolve_field() (the front-end render path); the |
| 358 | * editor calls this whenever Source/Field change. |
| 359 | * |
| 360 | * Response: { url: string } — empty string when nothing resolves. |
| 361 | */ |
| 362 | public static function rest_resolve_field(\WP_REST_Request $request) |
| 363 | { |
| 364 | $source = (string) $request->get_param('source'); |
| 365 | $field = (string) $request->get_param('field'); |
| 366 | $post_id = absint($request->get_param('post_id')); |
| 367 | |
| 368 | $url = self::resolve_field($source, $field, $post_id ?: null); |
| 369 | |
| 370 | return rest_ensure_response([ |
| 371 | 'url' => is_string($url) ? esc_url_raw($url) : '', |
| 372 | ]); |
| 373 | } |
| 374 | } |
| 375 |