PluginProbe
seQura / 4.1.3
seQura v4.1.3
4.3.4 4.3.3 4.3.2 4.3.1 trunk 2.0.0 2.0.10 2.0.11 2.0.12 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0.0 3.0.2 3.0.5 3.0.6 3.0.7 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 4.0.0 All 30 releases
sequra / src / Controllers / Hooks / Product / class-product-controller.php

class-product-controller.php in seQura 4.1.3, at src/Controllers/Hooks/Product/class-product-controller.php

434 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 * Product Controller
4 *
5 * @package SeQura/WC
6 * @subpackage SeQura/WC/Controllers
7 */
8
9 namespace SeQura\WC\Controllers\Hooks\Product;
10
11 use SeQura\Core\Infrastructure\Logger\LogContextData;
12 use SeQura\Core\Infrastructure\Utility\RegexProvider;
13 use SeQura\WC\Controllers\Controller;
14 use SeQura\WC\Services\Log\Interface_Logger_Service;
15 use SeQura\WC\Services\Product\Interface_Product_Service;
16 use SeQura\WC\Services\Widgets\Interface_Widgets_Service;
17 use WC_Product;
18 use WP_Post;
19
20 /**
21 * Product Controller implementation
22 *
23 * @phpstan-import-type WidgetDataArray from Interface_Widgets_Service
24 */
25 class Product_Controller extends Controller implements Interface_Product_Controller {
26
27 private const FIELD_NAME_IS_BANNED = 'is_sequra_banned';
28 private const FIELD_NAME_IS_SERVICE = 'is_sequra_service';
29 private const FIELD_NAME_SERVICE_END_DATE = 'sequra_service_end_date';
30 private const FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE = 'sequra_desired_first_charge_date';
31 private const FIELD_NAME_REGISTRATION_AMOUNT = 'sequra_registration_amount';
32 private const NONCE_SEQURA_PRODUCT = '_sequra_product_nonce';
33
34 /**
35 * Product service
36 *
37 * @var Interface_Product_Service
38 */
39 private $product_service;
40
41 /**
42 * RegEx service
43 *
44 * @var RegexProvider
45 */
46 private $regex;
47
48 /**
49 * Cached available widgets for product page
50 *
51 * @var array<WidgetDataArray[]>
52 */
53 private $available_widgets_for_product_page;
54
55 /**
56 * Widgets service
57 *
58 * @var Interface_Widgets_Service
59 */
60 private $widgets_service;
61
62 /**
63 * Constructor
64 */
65 public function __construct(
66 Interface_Logger_Service $logger,
67 string $templates_path,
68 Interface_Product_Service $product_service,
69 RegexProvider $regex,
70 Interface_Widgets_Service $widgets_service
71 ) {
72 parent::__construct( $logger, $templates_path );
73 $this->logger = $logger;
74 $this->product_service = $product_service;
75 $this->regex = $regex;
76 $this->widgets_service = $widgets_service;
77 $this->available_widgets_for_product_page = array();
78 }
79
80 /**
81 * Handle the widget shortcode callback
82 * [sequra_widget]
83 * Expected attributes:
84 * - product: The seQura product identifier. Required.
85 * - campaign: The seQura campaign name. Optional.
86 * - dest: A CSS selector to place the widget. Optional. If not provided, the widget will be placed in the same container as the shortcode.
87 * - product_id: The WooCommerce product identifier. Optional.
88 * - price: A CSS selector to get the product price or the price itself as a string. Optional.
89 * - alt_price: An alternative CSS selector to retrieve the product price for special product page layouts. Optional.
90 * - is_alt_price: A CSS selector to determine if the product has an alternative price. Optional.
91 * - reg_amount: The registration amount. Optional.
92 * - theme: The theme to use. Accepted values are: L, R, legacy, legacyL, legacyR, minimal, minimalL, minimalR or JSON formatted string. Optional.
93 * - min_amount: The minimum amount to display the widget. Optional.
94 * - max_amount: The maximum amount to display the widget. Optional.
95 *
96 * @param array<string, string> $atts The shortcode attributes
97 * @return string
98 */
99 public function do_widget_shortcode( $atts ) {
100 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
101 $atts = (array) $atts;
102
103 // Check for required attributes.
104 foreach ( array( 'product' ) as $required ) {
105 if ( ! isset( $atts[ $required ] ) ) {
106 $this->logger->log_error( "\"$required\" attribute is required", __FUNCTION__, __CLASS__ );
107 return '';
108 }
109 }
110
111 $product_id = intval( $atts['product_id'] ?? 0 );
112 $product = $atts['product'];
113 $campaign = isset( $atts['campaign'] ) && '' !== $atts['campaign'] ? $atts['campaign'] : '';
114 $widget = $this->get_widget_for_product( $product_id, $product, $campaign );
115 if ( ! $widget ) {
116 $this->logger->log_info(
117 'Widget is disabled',
118 __FUNCTION__,
119 __CLASS__,
120 array(
121 new LogContextData( 'payment_method', $product ),
122 new LogContextData( 'campaign', $campaign ),
123 new LogContextData( 'product_id', $product_id ),
124 )
125 );
126 return '';
127 }
128
129 if ( ! $this->product_service->can_display_widget_for_method( $product_id, $product ) ) {
130 $this->logger->log_info(
131 'Widget cannot be displayed for product',
132 __FUNCTION__,
133 __CLASS__,
134 array(
135 new LogContextData( 'payment_method', $product ),
136 new LogContextData( 'campaign', $campaign ),
137 new LogContextData( 'product_id', $product_id ),
138 )
139 );
140 return '';
141 }
142
143 // Replace old attribute names introduced in 2.0.0 with the new ones.
144 $atts_replacements = array(
145 'variation_price' => 'alt_price',
146 'is_variable' => 'is_alt_price',
147 );
148
149 foreach ( $atts_replacements as $old => $new ) {
150 if ( isset( $atts[ $old ] ) ) {
151 $atts[ $new ] = $atts[ $old ];
152 unset( $atts[ $old ] );
153 }
154 }
155
156 $atts = \shortcode_atts(
157 array(
158 'product' => $widget['product'] ?? '',
159 'campaign' => $widget['campaign'] ?? '',
160 'theme' => $widget['theme'] ?? '',
161 'reverse' => $widget['reverse'] ?? '0',
162 'reg_amount' => $this->product_service->get_registration_amount( $product_id, true ),
163 'dest' => '',
164 'price' => $widget['priceSel'] ?? '',
165 'alt_price' => $widget['altPriceSel'] ?? '',
166 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
167 'min_amount' => $widget['minAmount'] ?? 0,
168 'max_amount' => $widget['maxAmount'] ?? null,
169 ),
170 $atts,
171 'sequra_widget'
172 );
173
174 ob_start();
175 \wc_get_template( 'front/widget.php', $atts, '', $this->templates_path );
176 return ob_get_clean();
177 }
178
179 /**
180 * Handle the cart widget shortcode callback
181 *
182 * @param array<string, string> $atts The shortcode attributes
183 * @return string
184 */
185 public function do_cart_widget_shortcode( $atts ) {
186 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
187
188 $widget = $this->widgets_service->get_widget_for_cart_page();
189 if ( ! $widget ) {
190 return '';
191 }
192
193 $atts = \shortcode_atts(
194 array(
195 'product' => $widget['product'] ?? '',
196 'campaign' => $widget['campaign'] ?? '',
197 'dest' => $widget['dest'] ?? '',
198 'theme' => $widget['theme'] ?? '',
199 'price' => $widget['priceSel'] ?? '',
200 'alt_price' => $widget['altPriceSel'] ?? '',
201 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
202 'message' => $widget['miniWidgetMessage'],
203 'message_below_limit' => $widget['miniWidgetBelowLimitMessage'],
204 'min_amount' => $widget['minAmount'] ?? 0,
205 'max_amount' => $widget['maxAmount'] ?? null,
206 'reg_amount' => 0,
207 'reverse' => 0,
208 ),
209 (array) $atts,
210 'sequra_cart_widget'
211 );
212
213 ob_start();
214 \wc_get_template( 'front/widget.php', $atts, '', $this->templates_path );
215 return ob_get_clean();
216 }
217
218 /**
219 * Handle the product listing widget shortcode callback
220 *
221 * @param array<string, string> $atts The shortcode attributes
222 * @return string
223 */
224 public function do_product_listing_widget_shortcode( $atts ) {
225 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
226 $widget = $this->widgets_service->get_widget_for_product_listing_page();
227 if ( ! $widget ) {
228 return '';
229 }
230
231 $atts = \shortcode_atts(
232 array(
233 'product' => $widget['product'] ?? '',
234 'campaign' => $widget['campaign'] ?? '',
235 'dest' => $widget['dest'],
236 'price' => $widget['priceSel'],
237 'message' => $widget['miniWidgetMessage'],
238 'message_below_limit' => $widget['miniWidgetBelowLimitMessage'],
239 'min_amount' => $widget['minAmount'] ?? 0,
240 'max_amount' => $widget['maxAmount'] ?? null,
241 ),
242 (array) $atts,
243 'sequra_product_listing_widget'
244 );
245
246 ob_start();
247 \wc_get_template( 'front/mini_widget.php', $atts, '', $this->templates_path );
248 return ob_get_clean();
249 }
250
251 /**
252 * Get available widgets for a product page from in-memory cache or CheckoutAPI
253 *
254 * @param int|string $product_id The product ID
255 * @return WidgetDataArray[] The available widgets
256 */
257 private function get_available_widgets_for_product_page( $product_id ): array {
258
259 $product_id = (string) $product_id;
260 if ( isset( $this->available_widgets_for_product_page[ $product_id ] ) ) {
261 return $this->available_widgets_for_product_page[ $product_id ];
262 }
263
264 $widgets = $this->widgets_service->get_widgets_for_product_page( $product_id );
265
266 if ( ! $widgets ) {
267 return array();
268 }
269
270 $this->available_widgets_for_product_page[ $product_id ] = $widgets;
271 return $widgets;
272 }
273
274 /**
275 * Get the widget for a product if enabled
276 *
277 * @param string|int $product_id The WooCommerce product ID. Pass and empty value to ignore it.
278 * @param string $product The seQura product identifier
279 * @param string $campaign The seQura campaign name. Use an empty string to ignore the campaign.
280 *
281 * @return WidgetDataArray|null The widget data if enabled, null otherwise
282 */
283 private function get_widget_for_product( $product_id, string $product, string $campaign = '' ): ?array {
284 /**
285 * Promotional widget data
286 *
287 * @var array<string, mixed> $widget */
288 foreach ( $this->get_available_widgets_for_product_page( $product_id ) as $widget ) {
289 if ( $widget['product'] === $product && $widget['campaign'] === $campaign ) {
290 return $widget;
291 }
292 }
293 return null;
294 }
295
296 /**
297 * Add [sequra_widget] to product page automatically
298 */
299 private function add_widget_shortcode_to_product_page(): void {
300 if ( ! \is_product() ) {
301 return;
302 }
303 /**
304 * The current product.
305 *
306 * @var WC_Product $product
307 */
308 global $product;
309
310 foreach ( $this->get_available_widgets_for_product_page( $product->get_id() ) as $widget ) {
311 $atts = array(
312 'product' => $widget['product'] ?? '',
313 'campaign' => $widget['campaign'] ?? '',
314 'product_id' => $product->get_id(),
315 'theme' => $widget['theme'] ?? '',
316 'dest' => $widget['dest'] ?? '',
317 'price' => $widget['priceSel'] ?? '',
318 'alt_price' => $widget['altPriceSel'] ?? '',
319 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
320 'min_amount' => $widget['minAmount'] ?? 0,
321 'max_amount' => $widget['maxAmount'] ?? null,
322
323 );
324 $atts_str = '';
325 foreach ( $atts as $key => $value ) {
326 $atts_str .= " $key='$value'";
327 }
328 echo \do_shortcode( "[sequra_widget $atts_str]" );
329 }
330 }
331
332 /**
333 * Add [sequra_cart_widget] to product page automatically
334 */
335 private function add_widget_shortcode_to_cart_page(): void {
336 if ( ! \is_cart() ) {
337 return;
338 }
339 echo \do_shortcode( '[sequra_cart_widget]' );
340 }
341
342 /**
343 * Add [sequra_product_listing_widget] to product archive automatically
344 */
345 private function add_widget_shortcode_to_product_listing_page(): void {
346 if ( ! \is_product_category() && ! \is_product_tag() && ! \is_shop() ) {
347 return;
348 }
349 echo \do_shortcode( '[sequra_product_listing_widget]' );
350 }
351
352 /**
353 * Add [sequra_widget] to product page automatically
354 * Add [sequra_cart_widget] to cart page automatically
355 *
356 * @return void
357 */
358 public function add_widget_shortcode_to_page() {
359 $this->logger->log_debug( 'Hook executed', __FUNCTION__, __CLASS__ );
360 if ( \did_action( 'before_woocommerce_sequra_add_widget_to_page' ) ) {
361 return;
362 }
363
364 /**
365 * Fires before the SeQura widget is added to the page to prevent multiple executions.
366 *
367 * @since 3.0.0
368 */
369 \do_action( 'before_woocommerce_sequra_add_widget_to_page' );
370
371 $this->add_widget_shortcode_to_product_page();
372 $this->add_widget_shortcode_to_cart_page();
373 $this->add_widget_shortcode_to_product_listing_page();
374 }
375
376 /**
377 * Add meta boxes to the product edit page
378 *
379 * @return void
380 */
381 public function add_meta_boxes() {
382 \add_meta_box( 'sequra_settings', esc_html__( 'seQura settings', 'sequra' ), array( $this, 'render_meta_boxes' ), 'product', 'side', 'default' );
383 }
384
385 /**
386 * Render the meta boxes
387 *
388 * @param WP_Post $post The post object
389 * @return void
390 */
391 public function render_meta_boxes( $post ) {
392 $args = array(
393 'is_banned' => $this->product_service->get_is_banned( $post->ID ),
394 'is_banned_field_name' => self::FIELD_NAME_IS_BANNED,
395 'enabled_for_services' => $this->product_service->is_enabled_for_services(),
396 'is_service' => $this->product_service->is_service( $post->ID ),
397 'is_service_field_name' => self::FIELD_NAME_IS_SERVICE,
398 'service_end_date_default' => $this->product_service->get_default_services_end_date(),
399 'service_end_date' => $this->product_service->get_service_end_date( $post->ID, true ),
400 'service_end_date_field_name' => self::FIELD_NAME_SERVICE_END_DATE,
401 'allow_payment_delay' => $this->product_service->is_allow_first_service_payment_delay(),
402 'service_desired_first_charge_date' => $this->product_service->get_desired_first_charge_date( $post->ID, true ) ?? '',
403 'service_desired_first_charge_date_field_name' => self::FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE,
404 'date_or_duration_regex' => $this->regex->getDateOrDurationRegex( false ),
405 'allow_registration_items' => $this->product_service->is_allow_service_registration_items(),
406 'service_registration_amount' => $this->product_service->get_registration_amount( $post->ID ),
407 'service_registration_amount_field_name' => self::FIELD_NAME_REGISTRATION_AMOUNT,
408 'nonce_name' => self::NONCE_SEQURA_PRODUCT,
409 );
410 \wc_get_template( 'admin/product_metabox.php', $args, '', $this->templates_path );
411 }
412
413 /**
414 * Save product meta
415 *
416 * @param int $post_id The post ID
417 * @return void
418 */
419 public function save_product_meta( $post_id ) {
420 if ( ! isset( $_POST[ self::NONCE_SEQURA_PRODUCT ] )
421 || ! \wp_verify_nonce( $_POST[ self::NONCE_SEQURA_PRODUCT ], -1 ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
422 || ! \current_user_can( 'edit_post', $post_id )
423 || \wp_doing_ajax() ) {
424 return;
425 }
426
427 $this->product_service->set_is_banned( $post_id, isset( $_POST[ self::FIELD_NAME_IS_BANNED ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_IS_BANNED ] ) ) : null );
428 $this->product_service->set_is_service( $post_id, isset( $_POST[ self::FIELD_NAME_IS_SERVICE ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_IS_SERVICE ] ) ) : null );
429 $this->product_service->set_service_end_date( $post_id, ! empty( $_POST[ self::FIELD_NAME_SERVICE_END_DATE ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_SERVICE_END_DATE ] ) ) : null );
430 $this->product_service->set_desired_first_charge_date( $post_id, ! empty( $_POST[ self::FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_SERVICE_DESIRED_FIRST_CHARGE_DATE ] ) ) : null );
431 $this->product_service->set_registration_amount( $post_id, ! empty( $_POST[ self::FIELD_NAME_REGISTRATION_AMOUNT ] ) ? \sanitize_text_field( \wp_unslash( $_POST[ self::FIELD_NAME_REGISTRATION_AMOUNT ] ) ) : null );
432 }
433 }
434