PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
bit-form / includes / Core / Integration / WooCommerce / RecordApiHelper.php

RecordApiHelper.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.3.1, at includes/Core/Integration/WooCommerce/RecordApiHelper.php

476 lines 15.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * WooCommerce Record Api
5 *
6 */
7
8 namespace BitCode\BitForm\Core\Integration\WooCommerce;
9
10 use BitCode\BitForm\Core\Database\FormEntryLogModel;
11 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
12 use BitCode\BitForm\Core\Util\FileHandler;
13 use WC_Product_Download;
14 use WP_Error;
15
16 /**
17 * Provide functionality for Record insert,upsert
18 */
19 class RecordApiHelper
20 {
21 private $_integrationID;
22
23 private $_logID;
24
25 private $_logResponse;
26
27 public function __construct($integId, $logID)
28 {
29 $this->_integrationID = $integId;
30 $this->_logID = $logID;
31 $this->_logResponse = new UtilApiResponse();
32 }
33
34 public function executeRecordApi($formID, $entryID, $module, $fieldValues, $fieldMap, $uploadFieldMap, $required)
35 {
36 $entryDetails = [
37 'formId' => $formID,
38 'entryId' => $entryID,
39 'fieldValues' => $fieldValues
40 ];
41 $fieldData = [];
42 foreach ($fieldMap as $fieldPair) {
43 if (!empty($fieldPair->wcField) && !empty($fieldPair->formField)) {
44 if ('custom' === $fieldPair->formField && isset($fieldPair->customValue)) {
45 $fieldData[$fieldPair->wcField] = $fieldPair->customValue;
46 } else {
47 $fieldData[$fieldPair->wcField] = $fieldValues[$fieldPair->formField];
48 }
49
50 if (in_array($fieldPair->wcField, $required) && empty($fieldValues[$fieldPair->formField])) {
51 /* translators: %1$s: field name, %2$s: WooCommerce module name. */
52 $error = new WP_Error('REQ_FIELD_EMPTY', wp_sprintf(__('%1$s is required for woocommerce %2$s', 'bit-form'), $fieldPair->wcField, $module));
53 $this->_logResponse->apiResponse($this->_logID, $this->_integrationID, ['type' => $module, 'type_name' => 'create'], 'validation', $error, $entryDetails);
54 return $error;
55 }
56 }
57 }
58
59 $model = new FormEntryLogModel();
60 $result = $model->entryLogCheck($entryID, $this->_integrationID);
61
62 $entry_type = 'create';
63
64 if (count($result) && !isset($result->errors['result_empty'])) {
65 $entry_type = 'edit';
66 $api_type = json_decode($result[0]->api_type);
67
68 if ($api_type->type === $module) {
69 $id = $result[0]->response_obj;
70 }
71 }
72
73 if ('product' === $module) {
74 if (!empty($fieldData['tags_input'])) {
75 $tags = explode(',', $fieldData['tags_input']);
76 unset($fieldData['tags_input']);
77 }
78
79 if (!empty($fieldData['post_category'])) {
80 $categories = explode(',', $fieldData['post_category']);
81 unset($fieldData['post_category']);
82 }
83
84 if (!empty($fieldData['_regular_price'])) {
85 $price = $fieldData['_regular_price'];
86 }
87
88 if (!empty($fieldData['_sale_price'])) {
89 $price = $fieldData['_sale_price'];
90 }
91
92 if (!empty($fieldData['product_type'])) {
93 $product_type = $fieldData['product_type'];
94 if ('external' === $product_type && !empty($fieldData['_product_url'])) {
95 $product_type = 'external';
96 } else {
97 $product_type = 'simple';
98 unset($fieldData['_product_url'], $fieldData['_button_text']);
99 }
100 unset($fieldData['product_type']);
101 }
102
103 $post_fields = [
104 'post_content', 'post_title', 'post_status', 'post_type', 'comment_status', 'post_password', 'menu_order', 'post_excerpt', 'post_date', 'post_date_gmt'
105 ];
106
107 $post_inputs = array_intersect_key($fieldData, array_flip($post_fields));
108 $meta_inputs = array_diff_key($fieldData, array_flip($post_fields));
109
110 $fieldData = $post_inputs;
111 $fieldData['post_type'] = $module;
112 $fieldData['meta_input'] = $meta_inputs;
113
114 if (!empty($fieldData['post_date']) || !empty($fieldData['post_date_gmt'])) {
115 $fieldData['post_status'] = 'future';
116 }
117
118 if (isset($id)) {
119 $fieldData['ID'] = $id;
120 }
121
122 $product_id = wp_insert_post($fieldData);
123
124 if (isset($product_type)) {
125 wp_set_object_terms($product_id, $product_type, 'product_type');
126 }
127
128 if (isset($price)) {
129 update_post_meta($product_id, '_price', $price);
130 }
131
132 if (isset($categories)) {
133 wp_set_object_terms($product_id, $categories, 'product_cat');
134 }
135
136 if (isset($tags)) {
137 wp_set_object_terms($product_id, $tags, 'product_tag');
138 }
139
140 if (is_wp_error($product_id) || !$product_id) {
141 $response = is_wp_error($product_id) ? $product_id->get_error_message() : 'error';
142 return $this->_logResponse->apiResponse($this->_logID, $this->_integrationID, ['type' => 'product', 'type_name' => $entry_type], 'error', $response, $entryDetails);
143 } else {
144 $this->_logResponse->apiResponse($this->_logID, $this->_integrationID, ['type' => 'product', 'type_name' => $entry_type], 'success', $product_id, $entryDetails);
145 }
146 }
147
148 if ('customer' === $module) {
149 $user_fields = ['user_pass', 'user_login', 'user_nicename', 'user_url', 'user_email', 'display_name', 'nickname', 'first_name', 'last_name', 'description', 'locale'];
150
151 $user_inputs = array_intersect_key($fieldData, array_flip($user_fields));
152 $meta_inputs = array_diff_key($fieldData, array_flip($user_fields));
153
154 $fieldData = $user_inputs;
155 $fieldData['role'] = $module;
156
157 if (isset($id)) {
158 $fieldData['ID'] = $id;
159 }
160
161 $user_id = wp_insert_user($fieldData);
162
163 if (is_wp_error($user_id) || !$user_id) {
164 $response = is_wp_error($user_id) ? $user_id->get_error_message() : 'error';
165 return $this->_logResponse->apiResponse($this->_logID, $this->_integrationID, ['type' => 'customer', 'type_name' => $entry_type], 'error', $response, $entryDetails);
166 } else {
167 $this->_logResponse->apiResponse($this->_logID, $this->_integrationID, ['type' => 'customer', 'type_name' => $entry_type], 'success', $user_id, $entryDetails);
168 }
169
170 foreach ($meta_inputs as $metaKey => $metaValue) {
171 update_user_meta($user_id, $metaKey, $metaValue);
172 }
173
174 // Fires WooCommerce core hook so other WC extensions can react to the customer update.
175 do_action('woocommerce_update_customer', $user_id);
176 }
177
178 $flag = null;
179 if (isset($product_id)) {
180 $basepath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
181
182 foreach ($uploadFieldMap as $uploadField) {
183 if (!empty($uploadField->formField) && !empty($uploadField->wcField)) {
184 if ('product_image' === $uploadField->wcField) {
185 $flag = 0;
186 }
187 if ('product_gallery' === $uploadField->wcField) {
188 $flag = 1;
189 }
190 if ('downloadable_files' === $uploadField->wcField) {
191 $flag = 2;
192 }
193
194 $attach_ids = '';
195
196 if (!empty($fieldValues[$uploadField->formField])) {
197 $uplaodFiles = $fieldValues[$uploadField->formField];
198 if ('string' === gettype($fieldValues[$uploadField->formField])) {
199 $decoded = json_decode($fieldValues[$uploadField->formField]);
200 $uplaodFiles = is_null($decoded) ? $uplaodFiles : $decoded;
201 }
202 // Field values may arrive as public file URLs (see IntegrationHandler::handleFileUrl),
203 // so reduce each to the stored file name before resolving against the entry directory.
204 if (is_array($uplaodFiles)) {
205 foreach ($uplaodFiles as $singleFile) {
206 $singleFile = basename(wp_parse_url($singleFile, PHP_URL_PATH) ?: $singleFile);
207 $url = $basepath . $singleFile;
208 $attach_id = $this->attach_product_attachments($product_id, $flag, $url, $singleFile);
209 if (1 === $flag && $attach_id) {
210 $attach_ids .= ',' . $attach_id;
211 }
212 }
213 } else {
214 $filename = basename(wp_parse_url($uplaodFiles, PHP_URL_PATH) ?: $uplaodFiles);
215 $url = $basepath . $filename;
216 $this->attach_product_attachments($product_id, $flag, $url, $filename);
217 }
218 }
219
220 if (1 === $flag) {
221 update_post_meta($product_id, '_product_image_gallery', $attach_ids);
222 }
223 }
224 }
225 }
226 }
227
228 public function upload_attachment($product_id, $url)
229 {
230 require_once ABSPATH . 'wp-admin/includes/image.php';
231
232 $image_data = self::readImageData($url);
233 if (empty($image_data)) {
234 return null;
235 }
236
237 $image_name = basename(wp_parse_url($url, PHP_URL_PATH) ?: $url);
238
239 $upload_dir = wp_upload_dir();
240 $unique_file_name = wp_unique_filename($upload_dir['path'], $image_name);
241 $filename = basename($unique_file_name);
242
243 if (wp_mkdir_p($upload_dir['path'])) {
244 $file = $upload_dir['path'] . '/' . $filename;
245 } else {
246 $file = $upload_dir['basedir'] . '/' . $filename;
247 }
248 file_put_contents($file, $image_data);
249
250 $wp_filetype = wp_check_filetype($filename, null);
251
252 $attachment = [
253 'post_mime_type' => $wp_filetype['type'],
254 'post_title' => sanitize_file_name($filename),
255 'post_content' => '',
256 'post_status' => 'inherit'
257 ];
258
259 $attach_id = wp_insert_attachment($attachment, $file, $product_id);
260
261 $attach_data = wp_generate_attachment_metadata($attach_id, $file);
262
263 wp_update_attachment_metadata($attach_id, $attach_data);
264
265 return $attach_id;
266 }
267
268 /**
269 * Read a mapped image, preferring the local file over any network request.
270 *
271 * The mapped value is an absolute path inside the entry's upload directory, so
272 * an HTTP fetch never resolved it: both wp_remote_get() and wp_safe_remote_get()
273 * reject a schemeless URL.
274 *
275 * @param string $url Absolute path, uploads URL, or external URL
276 *
277 * @return string|null Raw image bytes, or null when it can't be read
278 */
279 private static function readImageData($url)
280 {
281 $localPath = self::resolveLocalUploadPath($url);
282
283 if (!is_null($localPath)) {
284 $contents = file_get_contents($localPath);
285 return false === $contents ? null : $contents;
286 }
287
288 if (!self::isSafeRemoteUrl($url)) {
289 return null;
290 }
291
292 $response = wp_safe_remote_get($url);
293 if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) {
294 return null;
295 }
296
297 $body = wp_remote_retrieve_body($response);
298
299 return '' === $body ? null : $body;
300 }
301
302 /**
303 * Whether a remote URL is safe to fetch from a submitted value.
304 *
305 * The ranges wp_safe_remote_get() rejects depend on the WordPress version —
306 * link-local 169.254.0.0/16 (cloud metadata) reached core's list long after
307 * 6.5, which the plugin still supports. Checked here for one behaviour on
308 * every version; wp_safe_remote_get() still runs afterwards.
309 *
310 * DNS rebinding stays open: pinning the request to the resolved address is
311 * not something the WP HTTP API exposes.
312 *
313 * @param string $url
314 *
315 * @return bool
316 */
317 private static function isSafeRemoteUrl($url)
318 {
319 $parts = wp_parse_url($url);
320
321 if (empty($parts['host']) || empty($parts['scheme'])) {
322 return false;
323 }
324 if (!\in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
325 return false;
326 }
327 if (isset($parts['user']) || isset($parts['pass'])) {
328 return false;
329 }
330 if (isset($parts['port']) && !\in_array((int) $parts['port'], [80, 443, 8080], true)) {
331 return false;
332 }
333
334 $host = trim($parts['host'], '[].');
335
336 if (filter_var($host, FILTER_VALIDATE_IP)) {
337 $ip = $host;
338 } else {
339 $ip = gethostbyname($host);
340 // gethostbyname() returns the host unchanged when it cannot resolve
341 if ($ip === $host) {
342 return false;
343 }
344 }
345
346 // NO_PRIV covers 10/8, 172.16/12, 192.168/16, fc00::/7;
347 // NO_RES covers 0/8, 127/8, 169.254/16, 240/4, ::1, fe80::/10.
348 if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
349 return false;
350 }
351
352 // 100.64.0.0/10 (CGNAT) and 224.0.0.0/4 (multicast): missed by both flags.
353 $octets = array_map('intval', explode('.', $ip));
354 if (4 === \count($octets)) {
355 if ((100 === $octets[0] && 64 <= $octets[1] && 127 >= $octets[1])
356 || (224 <= $octets[0] && 239 >= $octets[0])) {
357 return false;
358 }
359 }
360
361 return true;
362 }
363
364 /**
365 * Resolve a mapped value to a readable file inside this site's uploads directory.
366 *
367 * Takes an absolute filesystem path (what the field mapping passes) or a URL
368 * under the uploads base. Anything escaping the uploads directory returns null
369 * and is treated as remote.
370 *
371 * @param mixed $url Mapped submission value; not guaranteed to be a string
372 *
373 * @return string|null Absolute readable path, or null
374 */
375 private static function resolveLocalUploadPath($url)
376 {
377 if (!is_string($url) || '' === $url) {
378 return null;
379 }
380
381 $uploads = wp_upload_dir();
382 if (!empty($uploads['error']) || empty($uploads['basedir'])) {
383 return null;
384 }
385
386 $baseDir = realpath($uploads['basedir']);
387 if (false === $baseDir) {
388 return null;
389 }
390 $baseDir = wp_normalize_path($baseDir);
391
392 $scheme = wp_parse_url($url, PHP_URL_SCHEME);
393 $candidate = null;
394
395 if (empty($scheme)) {
396 // how the mapped value actually arrives
397 $candidate = $url;
398 } elseif (!empty($uploads['baseurl'])) {
399 // defensive: a caller passing the file's public URL instead
400 $basePath = wp_parse_url($uploads['baseurl'], PHP_URL_PATH);
401 $urlPath = wp_parse_url($url, PHP_URL_PATH);
402 $baseHost = wp_parse_url($uploads['baseurl'], PHP_URL_HOST);
403 $host = wp_parse_url($url, PHP_URL_HOST);
404
405 if (
406 !empty($basePath) && !empty($urlPath) && !empty($host)
407 && strtolower($host) === strtolower((string) $baseHost)
408 ) {
409 $basePath = rtrim($basePath, '/') . '/';
410 if (0 === strpos($urlPath, $basePath)) {
411 $candidate = $uploads['basedir'] . DIRECTORY_SEPARATOR . rawurldecode(substr($urlPath, \strlen($basePath)));
412 }
413 }
414 }
415
416 if (is_null($candidate)) {
417 return null;
418 }
419
420 $resolved = realpath($candidate);
421 if (false === $resolved) {
422 return null;
423 }
424 $resolved = wp_normalize_path($resolved);
425
426 // realpath() collapsed any traversal; confirm it stayed inside
427 if (0 !== strpos($resolved, rtrim($baseDir, '/') . '/')) {
428 return null;
429 }
430
431 return is_file($resolved) && is_readable($resolved) ? $resolved : null;
432 }
433
434 public function attach_product_attachments($product_id, $flag, $url, $filename)
435 {
436 $attach_id = $this->upload_attachment($product_id, $url);
437 if (0 === $flag) {
438 set_post_thumbnail($product_id, $attach_id);
439 }
440
441 if (1 === $flag) {
442 return $attach_id;
443 }
444
445 if (2 === $flag) {
446 $this->attach_downloadable_attachments($product_id, $url, $filename);
447 }
448 }
449
450 public function attach_downloadable_attachments($product_id, $url, $filename)
451 {
452 if ('yes' !== get_post_meta($product_id, '_downloadable', true)) {
453 return false;
454 }
455
456 require_once dirname(WC_PLUGIN_FILE) . '/includes/wc-product-functions.php';
457
458 $attach_id = $this->upload_attachment($product_id, $url);
459 $download_id = md5($url);
460 $file_url = wp_get_attachment_url($attach_id);
461
462 $pd_object = new WC_Product_Download();
463 $pd_object->set_id($download_id);
464 $pd_object->set_name($filename);
465 $pd_object->set_file($file_url);
466
467 $product = wc_get_product($product_id);
468
469 $downloads = $product->get_downloads();
470 $downloads[$download_id] = $pd_object;
471
472 $product->set_downloads($downloads);
473 $product->save();
474 }
475 }
476