PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
← All changes | inc/modules/class-add-module.php +469 -246 1.8.22.3.2 View file →
@@ -1,246 +1,469 @@
1 -<?php
2 -
3 -if ( ! defined( 'ABSPATH' ) ) {
4 - exit; // Exit if accessed directly
5 -}
6 -
7 -class Merchant_Add_Module {
8 -
9 - /**
10 - * WooCommerce only.
11 - *
12 - */
13 - public $wc_only = false;
14 -
15 - /**
16 - * Module section.
17 - *
18 - */
19 - public $module_section = '';
20 -
21 - /**
22 - * Module id.
23 - *
24 - */
25 - public $module_id = '';
26 -
27 - /**
28 - * Module default settings.
29 - *
30 - */
31 - public $module_default_settings = array();
32 -
33 - /**
34 - * Module data.
35 - *
36 - */
37 - public $module_data = array();
38 -
39 - /**
40 - * Module options.
41 - *
42 - */
43 - public $module_options_path = '';
44 -
45 - /**
46 - * Whether the module has a shortcode or not.
47 - *
48 - * @var bool
49 - */
50 - public $has_shortcode = false;
51 -
52 - /**
53 - * Constructor.
54 - *
55 - */
56 - public function __construct() {
57 - // Add and expose the module into the plugin dashboard.
58 - add_filter( 'merchant_modules', array( $this, 'add_module' ) );
59 -
60 - // Add module options.
61 - add_filter( 'merchant_module_file_path', array( $this, 'add_module_options' ), 10, 2 );
62 -
63 - // Add class to body to identify if module is active or not.
64 - add_filter( 'admin_body_class', array( $this, 'add_module_activation_status_class' ), 10, 2 );
65 -
66 - // Handle modules list item class.
67 - add_filter( "merchant_admin_module_{$this->module_id}_list_item_class", array( $this, 'modules_list_item_class' ) );
68 -
69 - if ( $this->has_shortcode ) {
70 - add_shortcode( 'merchant_module_' . str_replace( '-', '_', $this->module_id ), array( $this, 'shortcode_handler' ) );
71 - }
72 - }
73 -
74 - /**
75 - * Active modules class handler.
76 - *
77 - */
78 - public function add_module_activation_status_class( $classes ) {
79 - if ( ! $this->is_module_settings_page() ) {
80 - return $classes;
81 - }
82 -
83 - if ( Merchant_Modules::is_module_active( $this->module_id ) ) {
84 - $classes = $classes . ' merchant-module-enabled';
85 - } else {
86 - $classes = $classes . ' merchant-module-disabled';
87 - }
88 -
89 - return $classes;
90 - }
91 -
92 - /**
93 - * Modules list item class.
94 - *
95 - * @param string $module_class
96 - *
97 - * @return string
98 - */
99 - public function modules_list_item_class( $module_class ) {
100 - if ( $this->wc_only && ! class_exists( 'Woocommerce' ) ) {
101 - $module_class = $module_class . ' merchant-module-wc-only';
102 - }
103 -
104 - return $module_class;
105 - }
106 -
107 - /**
108 - * Is module settings page.
109 - *
110 - * @return bool
111 - */
112 - public function is_module_settings_page() {
113 - return isset( $_GET['page'] ) && 'merchant' === $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
114 - && isset( $_GET['module'] ) && $this->module_id === $_GET['module']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
115 - }
116 -
117 - /**
118 - * Get module settings.
119 - *
120 - */
121 - public function get_module_settings() {
122 - $settings = get_option( 'merchant' ) ? get_option( 'merchant' ) : array();
123 -
124 - // Default settings.
125 - $defaults = $this->module_default_settings;
126 -
127 - if ( empty( $settings[ $this->module_id ] ) ) {
128 - $settings[ $this->module_id ] = $defaults;
129 - }
130 -
131 - // Parse settings with defaults.
132 - $settings = wp_parse_args( $settings[ $this->module_id ], $defaults );
133 -
134 - return $settings;
135 - }
136 -
137 - /**
138 - * Get preview URL
139 - *
140 - * @param array $args
141 - *
142 - * @return string
143 - */
144 - public function set_module_preview_url( $args = array() ) {
145 - // Mount preview url.
146 - $preview_url = site_url( '/' );
147 -
148 - // Type based preview url
149 - if ( isset( $args['type'] ) ) {
150 - switch ( $args['type'] ) {
151 - case 'shop':
152 - if ( function_exists( 'wc_get_page_id' ) ) {
153 - $preview_url = get_permalink( wc_get_page_id( 'shop' ) );
154 - }
155 - break;
156 -
157 - case 'product':
158 - $query_args = array(
159 - 'post_type' => 'product',
160 - 'posts_per_page' => 1,
161 - );
162 -
163 - if ( isset( $args['query'] ) ) {
164 - $products = ( new WP_Query( wp_parse_args( $args['query'], $query_args ) ) )->get_posts();
165 -
166 - // If no results can be found with the custom query,
167 - // then use the default args
168 - if ( empty( $products ) || ! isset( $products[0] ) ) {
169 - $products = ( new WP_Query( $query_args ) )->get_posts();
170 - }
171 - } else {
172 - $products = ( new WP_Query( $query_args ) )->get_posts();
173 - }
174 -
175 - if ( ! empty( $products ) && isset( $products[0] ) ) {
176 - $preview_url = get_permalink( $products[0] );
177 - }
178 -
179 - break;
180 - }
181 - }
182 -
183 - return $preview_url;
184 - }
185 -
186 - /**
187 - * Add module.
188 - *
189 - */
190 - public function add_module( $modules ) {
191 - $modules[ $this->module_section ]['modules'][ $this->module_id ] = $this->module_data;
192 -
193 - return $modules;
194 - }
195 -
196 - /**
197 - * Add module options.
198 - *
199 - */
200 - public function add_module_options( $module_path, $merchant_module ) {
201 - if ( $this->module_id === $merchant_module ) {
202 - return $this->module_options_path;
203 - }
204 -
205 - return $module_path;
206 - }
207 -
208 - /**
209 - * Display error message if the shortcode is placed in the wrong place.
210 - *
211 - * @return mixed|null
212 - */
213 - public function shortcode_placement_error() {
214 - /*
215 - * translators: %s: module id
216 - */
217 - $message = __( 'The shortcode <strong>[merchant_module_%s]</strong> can only be used on single product pages.', 'merchant' );
218 - $message = sprintf( $message, str_replace( '-', '_', $this->module_id ) );
219 - $message = wp_kses( $message, array(
220 - 'strong' => array(),
221 - ) );
222 -
223 - /**
224 - * Filter the shortcode error message html content.
225 - *
226 - * @param string $message_content
227 - * @param string $module_id
228 - *
229 - * @since 1.8
230 - */
231 - return apply_filters( 'merchant_module_shortcode_error_message_html',
232 - '<div class="merchant-shortcode-wrong-placement">' .
233 - $message
234 - . '</div>',
235 - $this->module->module_id );
236 - }
237 -
238 - /**
239 - * Check if shortcode is enabled.
240 - *
241 - * @return bool
242 - */
243 - public function is_shortcode_enabled() {
244 - return Merchant_Admin_Options::get( $this->module_id, 'use_shortcode', false );
245 - }
246 -}
1 +<?php
2 +
3 +if ( ! defined( 'ABSPATH' ) ) {
4 + exit; // Exit if accessed directly
5 +}
6 +
7 +class Merchant_Add_Module {
8 +
9 + /**
10 + * WooCommerce only.
11 + *
12 + */
13 + public $wc_only = false;
14 +
15 + /**
16 + * Module section.
17 + *
18 + */
19 + public $module_section = '';
20 +
21 + /**
22 + * Module id.
23 + *
24 + */
25 + public $module_id = '';
26 +
27 + /**
28 + * Module default settings.
29 + *
30 + */
31 + public $module_default_settings = array();
32 +
33 + /**
34 + * Module data.
35 + *
36 + */
37 + public $module_data = array();
38 +
39 + /**
40 + * AI prompt examples shown in the "Ask AI" popup for this module.
41 + *
42 + * Subclasses set this in their constructor with a 'blurb' string
43 + * and a 'prompts' array of example prompts.
44 + *
45 + * @var array
46 + */
47 + protected $ai_examples = array();
48 +
49 + /**
50 + * Module options.
51 + *
52 + */
53 + public $module_options_path = '';
54 +
55 + /**
56 + * Whether the module has a shortcode or not.
57 + *
58 + * @var bool
59 + */
60 + public $has_shortcode = false;
61 +
62 + protected $has_analytics = false;
63 +
64 + /**
65 + * Option group definitions cache.
66 + *
67 + * @var array<int, array<string, mixed>>|null
68 + */
69 + protected $option_groups = null;
70 +
71 + /**
72 + * Initialize the module's option group definitions.
73 + *
74 + * Subclasses override this to define their settings panels.
75 + * Each element in the returned array corresponds to one settings
76 + * section in the admin UI.
77 + *
78 + * @return array<int, array<string, mixed>> Array of option group arrays, each with 'module', 'title', and 'fields' keys.
79 + */
80 + protected function init_option_groups() {
81 + return array();
82 + }
83 +
84 + /**
85 + * Get the module's option group definitions.
86 + *
87 + * Returns cached definitions if available. On first call,
88 + * initializes from the subclass and applies the filter for
89 + * third-party extensibility.
90 + *
91 + * @return array<int, array<string, mixed>> Filtered option group definitions.
92 + */
93 + public function get_option_groups() {
94 + if ( $this->option_groups !== null ) {
95 + return $this->option_groups;
96 + }
97 +
98 + $groups = $this->init_option_groups();
99 +
100 + /**
101 + * Filter a module's option group definitions.
102 + *
103 + * Allows third-party plugins to add, remove, or modify
104 + * the field definitions for any module.
105 + *
106 + * @param array<int, array<string, mixed>> $groups Array of option group arrays.
107 + *
108 + * @since 2.3.0
109 + */
110 + $this->option_groups = apply_filters(
111 + "merchant_{$this->module_id}_option_groups",
112 + $groups
113 + );
114 +
115 + return $this->option_groups;
116 + }
117 +
118 +
119 + /**
120 + * Constructor.
121 + *
122 + */
123 + public function __construct() {
124 + // Add and expose the module into the plugin dashboard.
125 + add_filter( 'merchant_modules', array( $this, 'add_module' ) );
126 +
127 + // Add module options.
128 + add_filter( 'merchant_module_file_path', array( $this, 'add_module_options' ), 10, 2 );
129 +
130 + // Add class to body to identify if module is active or not.
131 + add_filter( 'admin_body_class', array( $this, 'add_module_activation_status_class' ), 10, 2 );
132 +
133 + // Handle modules list item class.
134 + add_filter( "merchant_admin_module_{$this->module_id}_list_item_class", array( $this, 'modules_list_item_class' ) );
135 +
136 + if ( $this->has_shortcode ) {
137 + add_action( 'wp', array( $this, 'setup_product_object' ) );
138 + add_shortcode( 'merchant_module_' . str_replace( '-', '_', $this->module_id ), array( $this, 'shortcode_handler' ) );
139 + }
140 +
141 + // Remove merchant shortcodes from Botiga product card short description
142 + add_filter( 'botiga_loop_product_elements', function( $elements ) {
143 + add_filter( 'get_the_excerpt', function( $excerpt ) {
144 + return preg_replace( '/\[merchant_[^]]+]/', '', $excerpt );
145 + } );
146 +
147 + return $elements;
148 + } );
149 + }
150 +
151 + /**
152 + * Check if the module has analytics.
153 + *
154 + * @return bool
155 + */
156 + public function has_analytics() {
157 + return $this->has_analytics;
158 + }
159 +
160 + /**
161 + * Get the module's AI prompt examples.
162 + *
163 + * @return array Array with 'blurb' and 'prompts' keys, or an empty array if the module has none.
164 + */
165 + public function get_ai_examples() {
166 + /**
167 + * Filter a module's AI prompt examples.
168 + *
169 + * @param array $ai_examples Array with 'blurb' and 'prompts' keys.
170 + *
171 + * @since 2.3.0
172 + */
173 + return apply_filters( "merchant_{$this->module_id}_ai_examples", $this->ai_examples );
174 + }
175 +
176 + /**
177 + * Get all analytics metrics and allow modules to filter them.
178 + *
179 + * @return array List of available metrics.
180 + */
181 + public function analytics_metrics() {
182 + /**
183 + * Hook: merchant_analytics_module_metrics
184 + *
185 + * @param array $metrics List of available metrics.
186 + * @param string $module_id Module ID.
187 + *
188 + * @since 2.0
189 + */
190 + return apply_filters( 'merchant_analytics_module_metrics', $this->default_analytics_metrics(), $this->module_id, $this );
191 + }
192 +
193 + /**
194 + * Get analytics metrics.
195 + *
196 + * @return array List of available metrics.
197 + */
198 + protected function default_analytics_metrics() {
199 + return array(
200 + 'campaigns' => true,
201 + 'impressions' => true,
202 + 'clicks' => true,
203 + 'ctr' => true,
204 + 'revenue' => true,
205 + 'orders_count' => true,
206 + 'aov' => true,
207 + );
208 + }
209 +
210 + /**
211 + * Active modules class handler.
212 + *
213 + */
214 + public function add_module_activation_status_class( $classes ) {
215 + if ( ! $this->is_module_settings_page() ) {
216 + return $classes;
217 + }
218 +
219 + if ( Merchant_Modules::is_module_active( $this->module_id ) ) {
220 + $classes = $classes . ' merchant-module-enabled';
221 + } else {
222 + $classes = $classes . ' merchant-module-disabled';
223 + }
224 +
225 + return $classes;
226 + }
227 +
228 + /**
229 + * Modules list item class.
230 + *
231 + * @param string $module_class
232 + *
233 + * @return string
234 + */
235 + public function modules_list_item_class( $module_class ) {
236 + if ( $this->wc_only && ! class_exists( 'Woocommerce' ) ) {
237 + $module_class = $module_class . ' merchant-module-wc-only';
238 + }
239 +
240 + return $module_class;
241 + }
242 +
243 + /**
244 + * Is module settings page.
245 + *
246 + * @return bool
247 + */
248 + public function is_module_settings_page() {
249 + return isset( $_GET['page'] ) && 'merchant' === $_GET['page'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
250 + && isset( $_GET['module'] ) && $this->module_id === $_GET['module']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
251 + }
252 +
253 + /**
254 + * Get module settings.
255 + *
256 + */
257 + public function get_module_settings() {
258 + $settings = get_option( 'merchant', array() );
259 +
260 + // Default settings.
261 + $defaults = $this->module_default_settings;
262 +
263 + if ( empty( $settings[ $this->module_id ] ) ) {
264 + $settings[ $this->module_id ] = $defaults;
265 + }
266 +
267 + // Parse settings with defaults.
268 + // Todo: check if recursive_parse_args() works for all modules and remove the condition.
269 + $settings = $this->module_id === 'product-labels' ? $this->recursive_parse_args( $settings[ $this->module_id ], $defaults ) : wp_parse_args( $settings[ $this->module_id ], $defaults );
270 +
271 + /**
272 + * Hook: merchant_module_settings
273 + *
274 + * @param array $settings Module settings.
275 + * @param string $module_id Module ID.
276 + *
277 + * @since 1.9.16
278 + */
279 + $settings = apply_filters( 'merchant_module_settings', $settings, $this->module_id );
280 +
281 + return $settings;
282 + }
283 +
284 + /**
285 + * Update module settings.
286 + *
287 + * @param array $module_settings
288 + *
289 + * @return void
290 + */
291 + public function update_module_settings( $module_settings ) {
292 + $settings = get_option( 'merchant', array() );
293 +
294 + $settings[ $this->module_id ] = $module_settings;
295 +
296 + /**
297 + * Hook: merchant_module_settings_update
298 + *
299 + * @param array $settings Module settings.
300 + * @param string $module_id Module ID.
301 + *
302 + * @since 2.0.0
303 + */
304 + $settings = apply_filters( 'merchant_module_settings_update', $settings, $this->module_id );
305 +
306 + update_option( 'merchant', $settings );
307 + }
308 +
309 + /**
310 + * Add module.
311 + *
312 + */
313 + public function add_module( $modules ) {
314 + $modules[ $this->module_section ]['modules'][ $this->module_id ] = $this->module_data;
315 +
316 + return $modules;
317 + }
318 +
319 + /**
320 + * Add module options.
321 + *
322 + * For migrated modules (those with option groups defined),
323 + * renders settings directly from get_option_groups().
324 + * Unmigrated modules fall back to including $module_options_path.
325 + *
326 + * @param string $module_path Default file path.
327 + * @param string $merchant_module Module ID being rendered.
328 + *
329 + * @return string File path to include, or empty string if already rendered.
330 + */
331 + public function add_module_options( $module_path, $merchant_module ) {
332 + if ( $this->module_id !== $merchant_module ) {
333 + return $module_path;
334 + }
335 +
336 + $groups = $this->get_option_groups();
337 +
338 + if ( ! empty( $groups ) ) {
339 + $this->before_render_option_groups();
340 +
341 + foreach ( $groups as $group ) {
342 + Merchant_Admin_Options::create( $group );
343 + }
344 +
345 + $this->after_render_option_groups();
346 +
347 + return '';
348 + }
349 +
350 + return $this->module_options_path;
351 + }
352 +
353 + /**
354 + * Called before option groups are rendered in the admin.
355 + *
356 + * Override in subclasses that need to fire actions
357 + * before settings fields (e.g., help banners).
358 + *
359 + * @return void
360 + */
361 + protected function before_render_option_groups() {}
362 +
363 + /**
364 + * Called after option groups are rendered in the admin.
365 + *
366 + * Override in subclasses that need to fire actions
367 + * after settings fields.
368 + *
369 + * @return void
370 + */
371 + protected function after_render_option_groups() {}
372 +
373 + /**
374 + * Display error message if the shortcode is placed in the wrong place.
375 + *
376 + * @return mixed|null
377 + */
378 + public function shortcode_placement_error() {
379 + /*
380 + * translators: %s: module id
381 + */
382 + $message = __( 'The shortcode <strong>[merchant_module_%s]</strong> can only be used on single product pages.', 'merchant' );
383 + $message = sprintf( $message, str_replace( '-', '_', $this->module_id ) );
384 + $message = wp_kses( $message, array(
385 + 'strong' => array(),
386 + ) );
387 +
388 + /**
389 + * Filter the shortcode error message html content.
390 + *
391 + * @param string $message_content
392 + * @param string $module_id
393 + *
394 + * @since 1.8
395 + */
396 + return apply_filters( 'merchant_module_shortcode_error_message_html',
397 + '<div class="merchant-shortcode-wrong-placement">' .
398 + $message
399 + . '</div>',
400 + $this->module_id );
401 + }
402 +
403 + /**
404 + * Check if shortcode is enabled.
405 + *
406 + * @return bool
407 + */
408 + public function is_shortcode_enabled() {
409 +
410 + /**
411 + * Hook 'merchant_{$this->module_id}_is_shortcode_enabled'
412 + *
413 + * @since 1.9.3
414 + */
415 + return apply_filters( "merchant_{$this->module_id}_is_shortcode_enabled", Merchant_Admin_Options::get( $this->module_id, 'use_shortcode', false ) );
416 + }
417 +
418 + /**
419 + * Recursively merges user-defined arguments into default arguments.
420 + *
421 + * @param $args
422 + * @param $defaults
423 + *
424 + * @return mixed
425 + */
426 + private function recursive_parse_args( $args, $defaults ) {
427 + $result = $defaults;
428 +
429 + foreach ( $args as $key => $value ) {
430 + // If the value is an array and the corresponding default is also an array, merge them recursively.
431 + if ( is_array( $value ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
432 + $result[ $key ] = $this->recursive_parse_args( $value, $result[ $key ] );
433 + } else {
434 + $result[ $key ] = $value;
435 + }
436 + }
437 +
438 + return $result;
439 + }
440 +
441 + /**
442 + * Ensure $product is an object in the Breakdance builder editor.
443 + * If $product is a string, convert it to a WooCommerce product object.
444 + *
445 + * @return void
446 + */
447 + public function setup_product_object() {
448 + if ( ! class_exists( 'WooCommerce' ) ) {
449 + return;
450 + }
451 +
452 + if ( ! is_product() ) {
453 + return;
454 + }
455 +
456 + global $product;
457 +
458 + // Check if $product is a string and not already an object
459 + if ( ! is_object( $product ) && is_string( $product ) ) {
460 + // Retrieve the product object by slug
461 + $product_object = wc_get_product( get_page_by_path( $product, OBJECT, 'product' ) );
462 +
463 + // Update global $product with the retrieved product object if found
464 + if ( $product_object ) {
465 + $product = $product_object;
466 + }
467 + }
468 + }
469 +}