PluginProbe
seQura / 4.0.0
seQura v4.0.0
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.0.0, at src/Controllers/Hooks/Product/class-product-controller.php

434 lines 14.9 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. Required.
87 * - product_id: The WooCommerce product identifier. Required
88 * - price: A CSS selector to get the product price. 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', 'product_id' ) 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 = (int) $atts['product_id'];
112 $product = $atts['product'];
113 $campaign = isset( $atts['campaign'] ) && '' !== $atts['campaign'] ? $atts['campaign'] : '';
114
115 if ( ! $this->is_widget_enabled_for_product( $product_id, $product, $campaign ) ) {
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 // Replace old attribute names introduced in 2.0.0 with the new ones.
130 $atts_replacements = array(
131 'variation_price' => 'alt_price',
132 'is_variable' => 'is_alt_price',
133 );
134
135 foreach ( $atts_replacements as $old => $new ) {
136 if ( isset( $atts[ $old ] ) ) {
137 $atts[ $new ] = $atts[ $old ];
138 unset( $atts[ $old ] );
139 }
140 }
141
142 $atts = \shortcode_atts(
143 array(
144 'product' => '',
145 'campaign' => '',
146 'product_id' => '',
147 'theme' => '',
148 'reverse' => 0,
149 'reg_amount' => $this->product_service->get_registration_amount( $product_id, true ),
150 'dest' => '',
151 'price' => '',
152 'alt_price' => '',
153 'is_alt_price' => '',
154 'min_amount' => 0,
155 'max_amount' => null,
156 ),
157 $atts,
158 'sequra_widget'
159 );
160
161 if ( ! $this->product_service->can_display_widget_for_method( $product_id, $product ) ) {
162 $this->logger->log_info(
163 'Widget cannot be displayed for product',
164 __FUNCTION__,
165 __CLASS__,
166 array(
167 new LogContextData( 'payment_method', $product ),
168 new LogContextData( 'campaign', $campaign ),
169 new LogContextData( 'product_id', $product_id ),
170 )
171 );
172 return '';
173 }
174
175 ob_start();
176 \wc_get_template( 'front/widget.php', $atts, '', $this->templates_path );
177 return ob_get_clean();
178 }
179
180 /**
181 * Handle the cart widget shortcode callback
182 *
183 * @param array<string, string> $atts The shortcode attributes
184 * @return string
185 */
186 public function do_cart_widget_shortcode( $atts ) {
187 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
188
189 $widget = $this->widgets_service->get_widget_for_cart_page();
190 if ( ! $widget ) {
191 return '';
192 }
193
194 $atts = \shortcode_atts(
195 array(
196 'product' => $widget['product'] ?? '',
197 'campaign' => $widget['campaign'] ?? '',
198 'dest' => $widget['dest'] ?? '',
199 'theme' => $widget['theme'] ?? '',
200 'price' => $widget['priceSel'] ?? '',
201 'alt_price' => $widget['altPriceSel'] ?? '',
202 'is_alt_price' => $widget['altTriggerSelector'] ?? '',
203 'message' => $widget['miniWidgetMessage'],
204 'message_below_limit' => $widget['miniWidgetBelowLimitMessage'],
205 'min_amount' => $widget['minAmount'] ?? 0,
206 'max_amount' => $widget['maxAmount'] ?? null,
207 'reg_amount' => 0,
208 'reverse' => 0,
209 ),
210 (array) $atts,
211 'sequra_cart_widget'
212 );
213
214 ob_start();
215 \wc_get_template( 'front/widget.php', $atts, '', $this->templates_path );
216 return ob_get_clean();
217 }
218
219 /**
220 * Handle the product listing widget shortcode callback
221 *
222 * @param array<string, string> $atts The shortcode attributes
223 * @return string
224 */
225 public function do_product_listing_widget_shortcode( $atts ) {
226 $this->logger->log_debug( 'Shortcode called', __FUNCTION__, __CLASS__ );
227 $widget = $this->widgets_service->get_widget_for_product_listing_page();
228 if ( ! $widget ) {
229 return '';
230 }
231
232 $atts = \shortcode_atts(
233 array(
234 'product' => $widget['product'] ?? '',
235 'campaign' => $widget['campaign'] ?? '',
236 'dest' => $widget['dest'],
237 'price' => $widget['priceSel'],
238 'message' => $widget['miniWidgetMessage'],
239 'message_below_limit' => $widget['miniWidgetBelowLimitMessage'],
240 'min_amount' => $widget['minAmount'] ?? 0,
241 'max_amount' => $widget['maxAmount'] ?? null,
242 ),
243 (array) $atts,
244 'sequra_product_listing_widget'
245 );
246
247 ob_start();
248 \wc_get_template( 'front/mini_widget.php', $atts, '', $this->templates_path );
249 return ob_get_clean();
250 }
251
252 /**
253 * Get available widgets for a product page from in-memory cache or CheckoutAPI
254 *
255 * @param int|string $product_id The product ID
256 * @return WidgetDataArray[] The available widgets
257 */
258 private function get_available_widgets_for_product_page( $product_id ): array {
259
260 $product_id = (string) $product_id;
261 if ( isset( $this->available_widgets_for_product_page[ $product_id ] ) ) {
262 return $this->available_widgets_for_product_page[ $product_id ];
263 }
264
265 $widgets = $this->widgets_service->get_widgets_for_product_page( $product_id );
266
267 if ( ! $widgets ) {
268 return array();
269 }
270
271 $this->available_widgets_for_product_page[ $product_id ] = $widgets;
272 return $widgets;
273 }
274
275 /**
276 * Check if the widget is enabled
277 *
278 * @param string|int $product_id The WooCommerce product ID
279 * @param string $product The seQura product identifier
280 * @param string $campaign The seQura campaign name. Use an empty string to ignore the campaign.
281 * @return bool
282 */
283 private function is_widget_enabled_for_product( $product_id, string $product, string $campaign = '' ): bool {
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 true;
291 }
292 }
293 return false;
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