PluginProbe
Tiered Pricing Table for WooCommerce / 8.0.2
Tiered Pricing Table for WooCommerce v8.0.2
8.0.2 7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 All 91 releases
tier-pricing-table / src / Addons / RequestAQuote / Frontend / API / SubmitQuoteEndpoint.php

SubmitQuoteEndpoint.php in Tiered Pricing Table for WooCommerce 8.0.2, at src/Addons/RequestAQuote/Frontend/API/SubmitQuoteEndpoint.php

272 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php namespace TierPricingTable\Addons\RequestAQuote\Frontend\API;
2
3 use TierPricingTable\Addons\RequestAQuote\CPT\QuoteRequestCPT;
4 use TierPricingTable\Addons\RequestAQuote\Models\QuoteRequest;
5 use TierPricingTable\Addons\RequestAQuote\Models\RequestQuoteForm;
6 use TierPricingTable\PriceManager;
7 use WP_REST_Controller;
8 use WP_REST_Request;
9 use WP_REST_Server;
10 use WP_Error;
11
12 class SubmitQuoteEndpoint extends WP_REST_Controller {
13
14 public function __construct() {
15 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
16 }
17
18 public function register_routes() {
19 register_rest_route( 'tier-pricing-table/v1', '/quote-request', array(
20 'methods' => WP_REST_Server::CREATABLE,
21 'callback' => array( $this, 'submitQuote' ),
22 'permission_callback' => array( $this, 'permissionCallback' ),
23 ) );
24 }
25
26 /**
27 * A public form: visitors may submit. Without reCAPTCHA keys the request must carry the REST nonce of
28 * the page that rendered the form; with keys, the handler verifies the reCAPTCHA token instead.
29 *
30 * @return true|WP_Error
31 */
32 public function permissionCallback( WP_REST_Request $request ) {
33 $globalSettings = get_option( 'tier_pricing_table_quote_global_settings', array() );
34
35 if ( ! empty( $globalSettings['recaptcha_site_key'] ) && ! empty( $globalSettings['recaptcha_secret_key'] ) ) {
36 return true;
37 }
38
39 $nonce = $request->get_param( '_wpnonce' ) ? sanitize_text_field( (string) $request->get_param( '_wpnonce' ) ) : (string) $request->get_header( 'x_wp_nonce' );
40
41 if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
42 return new WP_Error( 'invalid_nonce',
43 __( 'Security check failed. Please refresh the page and try again.', 'tier-pricing-table' ),
44 array( 'status' => 403 ) );
45 }
46
47 return true;
48 }
49
50 public function submitQuote( WP_REST_Request $request ) {
51 $params = $request->get_params();
52 $formId = isset( $params['form_id'] ) ? sanitize_text_field( $params['form_id'] ) : '';
53 $productId = isset( $params['product_id'] ) ? intval( $params['product_id'] ) : 0;
54
55 $form = RequestQuoteForm::get( $formId );
56 $product = wc_get_product( $productId );
57
58 if ( ! $product || ! $form ) {
59 return new WP_Error( 'invalid_data', __( 'Form ID or Product ID is missing.', 'tier-pricing-table' ),
60 array( 'status' => 400 ) );
61 }
62
63 // Additional Validation: Check if the product has this form attached
64 $pricingRule = PriceManager::getPricingRule( $productId );
65 $attachedFormId = $pricingRule->data['tier_pricing_table_quote_form_id'] ?? null;
66
67 if ( (string) $attachedFormId !== (string) $form->getId() ) {
68 return new WP_Error( 'invalid_form_attachment',
69 __( 'This form is not available for the selected product.', 'tier-pricing-table' ),
70 array( 'status' => 403 ) );
71 }
72
73 $globalSettings = get_option( 'tier_pricing_table_quote_global_settings', array() );
74 $siteKey = $globalSettings['recaptcha_site_key'] ?? null;
75 $secretKey = $globalSettings['recaptcha_secret_key'] ?? null;
76
77 if ( ! empty( $siteKey ) && ! empty( $secretKey ) ) {
78 $recaptchaResponse = isset( $params['g-recaptcha-response'] ) ? sanitize_text_field( $params['g-recaptcha-response'] ) : '';
79 if ( empty( $recaptchaResponse ) ) {
80 return new WP_Error( 'spam_detected',
81 __( 'Anti-Spam verification failed. Please try again.', 'tier-pricing-table' ),
82 array( 'status' => 400 ) );
83 }
84
85 $verifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
86 $verifyResponse = wp_remote_post( $verifyUrl, array(
87 'body' => array(
88 'secret' => $secretKey,
89 'response' => $recaptchaResponse,
90 ),
91 ) );
92
93 if ( is_wp_error( $verifyResponse ) ) {
94 return new WP_Error( 'spam_verification_error',
95 __( 'Could not verify reCAPTCHA.', 'tier-pricing-table' ), array( 'status' => 500 ) );
96 }
97
98 $verifyBody = wp_remote_retrieve_body( $verifyResponse );
99 $verifyData = json_decode( $verifyBody );
100
101 if ( ! $verifyData || ! isset( $verifyData->success ) || ! $verifyData->success ) {
102 return new WP_Error( 'spam_detected',
103 __( 'Anti-Spam verification failed. Token invalid.', 'tier-pricing-table' ),
104 array( 'status' => 400 ) );
105 }
106
107 // Check score (v3 only)
108 if ( isset( $verifyData->score ) && $verifyData->score < 0.5 ) {
109 return new WP_Error( 'spam_detected',
110 __( 'Anti-Spam verification failed. Score too low.', 'tier-pricing-table' ),
111 array( 'status' => 400 ) );
112 }
113 }
114 // without reCAPTCHA keys the REST nonce was verified by the permission callback
115
116 $quote = new QuoteRequest();
117
118 $quote->setProductId( $productId );
119 // translators: 1: Product name, 2: Date.
120 $quote->setTitle( sprintf( __( '%1$s - %2$s', 'tier-pricing-table' ), $product->get_name(),
121 wp_date( 'Y-m-d H:i:s' ) ) );
122
123 $content = "Quote Request for Product: " . $product->get_name() . " (ID: " . $productId . ")\n\n";
124
125 $customFields = array();
126 $customerEmail = '';
127 $quantity = 1;
128
129 // Capture all params into customFields except the internal ones
130 foreach ( $params as $key => $value ) {
131 if ( in_array( $key,
132 array( 'form_id', 'product_id', 'variation_id', '_wpnonce', 'g-recaptcha-response' ) ) ) {
133 continue;
134 }
135
136 if ( is_array( $value ) ) {
137 $sanitizedValue = implode( ', ', array_map( 'sanitize_textarea_field', $value ) );
138 } else {
139 $sanitizedValue = sanitize_textarea_field( $value );
140 }
141
142 // Enforce max lengths to prevent payload abuse
143 $maxLength = ( strpos( $key, 'message' ) !== false || strpos( $key, 'textarea' ) !== false ) ? 1500 : 255;
144 if ( mb_strlen( $sanitizedValue ) > $maxLength ) {
145 $sanitizedValue = mb_substr( $sanitizedValue, 0, $maxLength );
146 }
147
148 if ( $key === 'quantity' ) {
149 $quantity = (int) $sanitizedValue;
150 } elseif ( strpos( $key, 'email' ) !== false && empty( $customerEmail ) && is_email( $sanitizedValue ) ) {
151 $customerEmail = $sanitizedValue;
152 }
153
154 $fieldConfig = array( 'type' => 'text', 'label' => ucfirst( str_replace( '_', ' ', $key ) ) );
155 if ( $form ) {
156 foreach ( $form->getFields() as $formField ) {
157 if ( isset( $formField['name'] ) && $formField['name'] === $key ) {
158 $fieldConfig = $formField;
159 break;
160 }
161 }
162 }
163
164 $fieldConfig['value'] = $sanitizedValue;
165 $customFields[ $key ] = $fieldConfig;
166
167 $content .= ucfirst( $key ) . ": " . $sanitizedValue . "\n";
168 }
169
170 // Process Files
171 $fileUploadResult = $this->processFileUploads( $form, $customFields, $content );
172 if ( is_wp_error( $fileUploadResult ) ) {
173 return $fileUploadResult;
174 }
175
176 $quote->setCustomFields( $customFields );
177 $quote->setCustomerEmail( $customerEmail );
178 $quote->setQuantity( $quantity );
179 $quote->setContent( $content );
180 $quote->updateMetaData( '_form_id',
181 $formId ); // Save form ID as regular meta since it's not a primary property anymore
182
183 if ( get_current_user_id() > 0 ) {
184 $quote->setUserId( get_current_user_id() );
185 }
186
187 // Calculate tier price based on customer's context
188 $pricingRule = PriceManager::getPricingRule( $productId );
189 $tierPrice = $pricingRule->getTierPrice( $quantity, false );
190
191 if ( ! $tierPrice && $tierPrice <= 0 ) {
192 $tierPrice = $product->get_price();
193 }
194 $quote->setPrice( $tierPrice );
195
196 $postId = $quote->save();
197
198 if ( ! $postId ) {
199 return new WP_Error( 'insert_failed', __( 'Could not save the quote request.', 'tier-pricing-table' ),
200 array( 'status' => 500 ) );
201 }
202
203 // Trigger event for emails and other integrations
204 do_action( 'tiered_pricing_table/request_quote/quote_request_submitted', $postId );
205
206 return rest_ensure_response( array(
207 'success' => true,
208 'message' => __( 'Your quote request has been submitted successfully!', 'tier-pricing-table' ),
209 'post_id' => $postId,
210 ) );
211 }
212
213 private function processFileUploads( $form, &$customFields, &$content ) {
214 if ( empty( $_FILES ) ) {
215 return true;
216 }
217
218 if ( ! function_exists( 'wp_handle_upload' ) ) {
219 require_once( ABSPATH . 'wp-admin/includes/file.php' );
220 }
221
222 foreach ( $_FILES as $key => $file ) {
223 if ( empty( $file['name'] ) || $file['error'] !== UPLOAD_ERR_OK ) {
224 continue;
225 }
226
227 $fieldConfig = array( 'type' => 'file', 'label' => ucfirst( str_replace( '_', ' ', $key ) ) );
228 if ( $form ) {
229 foreach ( $form->getFields() as $formField ) {
230 if ( isset( $formField['name'] ) && $formField['name'] === $key ) {
231 $fieldConfig = $formField;
232 break;
233 }
234 }
235 }
236
237 // Validate Max Size
238 if ( ! empty( $fieldConfig['maxFileSize'] ) ) {
239 $maxBytes = $fieldConfig['maxFileSize'] * 1024 * 1024;
240 if ( $file['size'] > $maxBytes ) {
241 // translators: 1: File name, 2: Max file size in MB.
242 return new WP_Error( 'file_too_large', sprintf( __( 'File %1$s exceeds the maximum allowed size of %2$s MB.', 'tier-pricing-table' ), esc_html( $file['name'] ), esc_html( $fieldConfig['maxFileSize'] ) ), array( 'status' => 400 ) );
243 }
244 }
245
246 // Validate Extension
247 if ( ! empty( $fieldConfig['allowedTypes'] ) ) {
248 $allowedExts = array_map( 'strtolower', array_map( 'trim', explode( ',', str_replace( '.', '', $fieldConfig['allowedTypes'] ) ) ) );
249 $fileExt = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
250 if ( ! in_array( $fileExt, $allowedExts, true ) ) {
251 // translators: 1: File extension, 2: Allowed extensions.
252 return new WP_Error( 'invalid_file_type', sprintf( __( 'File type .%1$s is not allowed. Allowed: %2$s', 'tier-pricing-table' ), esc_html( $fileExt ), esc_html( $fieldConfig['allowedTypes'] ) ), array( 'status' => 400 ) );
253 }
254 }
255
256 // Upload
257 $uploadOverrides = array( 'test_form' => false );
258 $movefile = wp_handle_upload( $file, $uploadOverrides );
259
260 if ( $movefile && ! isset( $movefile['error'] ) ) {
261 $fieldConfig['value'] = $movefile['url'];
262 $fieldConfig['file_path'] = $movefile['file'];
263 $customFields[ $key ] = $fieldConfig;
264 $content .= ucfirst( $key ) . ": " . $movefile['url'] . "\n";
265 } else {
266 return new WP_Error( 'upload_failed', $movefile['error'] ?? __( 'File upload failed.', 'tier-pricing-table' ), array( 'status' => 500 ) );
267 }
268 }
269
270 return true;
271 }
272 }