PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 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 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Includes / Classes / DynamicFieldResolver.php
embedpress / EmbedPress / Includes / Classes Last commit date
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