PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 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 All 139 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 V3.3.2, at includes/Core/Integration/WooCommerce/RecordApiHelper.php

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