PluginProbe
Tiered Pricing Table for WooCommerce / 7.1.1
Tiered Pricing Table for WooCommerce v7.1.1
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 7.1.1, at src/Addons/RequestAQuote/Frontend/API/SubmitQuoteEndpoint.php

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