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
merchant / admin / classes / class-merchant-white-label-settings.php

class-merchant-white-label-settings.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.2, at admin/classes/class-merchant-white-label-settings.php

596 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Merchant White Label settings section.
4 *
5 * Renders the White Label controls on the Global Settings page and keeps the
6 * values in the aThemes White Label plugin's own option, so both UIs read and
7 * write the same data.
8 *
9 * @package Merchant
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 if ( ! class_exists( 'Merchant_White_Label_Settings' ) ) {
17
18 class Merchant_White_Label_Settings {
19
20 /**
21 * Plugin slugs whose plugin-list rows are re-branded.
22 *
23 * One set of fields covers both, so every slug gets the same values.
24 *
25 * @var string[]
26 */
27 const PLUGIN_SLUGS = array( 'merchant', 'merchant-pro' );
28
29 /**
30 * The slug whose option keys the shared fields are stored under.
31 *
32 * Field IDs are AWL option keys verbatim, and AWL keys are per plugin, so
33 * the shared field borrows the free plugin's key and the save mirror copies
34 * the value out to the rest.
35 *
36 * @var string
37 */
38 const PRIMARY_SLUG = self::PLUGIN_SLUGS[0];
39
40 /**
41 * Settings module ID.
42 *
43 * @var string
44 */
45 const MODULE = 'white-label';
46
47 /**
48 * Where the plugin's switch, as the sync last saw it, is remembered.
49 *
50 * @var string
51 */
52 const LAST_SEEN_OPTION = 'merchant_white_label_awl_last_seen';
53
54 /**
55 * The row details an agency can hide, as the AWL key fragments.
56 *
57 * @var string[]
58 */
59 const HIDE_TOGGLES = array( 'author', 'version', 'changelog', 'actions' );
60
61 /**
62 * Constructor.
63 */
64 public function __construct() {
65 // Priority 20 renders this after the License section (priority 10)
66 // and before Analytics, which follows the same do_action() call.
67 add_action( 'merchant_admin_settings_before_options', array( $this, 'create_settings' ), 20 );
68 add_action( 'merchant_options_saved_' . self::MODULE, array( __CLASS__, 'mirror_to_plugin_option' ) );
69
70 // Priority 20 runs after Merchant Pro's licence tier sync, so the
71 // entitlement answer is settled before the switch is written.
72 add_action( 'admin_init', array( __CLASS__, 'sync_switch_from_plugin' ), 20 );
73
74 // The modal is a dialog, not a settings field: printing it in the
75 // footer keeps it out of the locked section's disabling pass and out
76 // of the settings form.
77 add_action( 'admin_footer', array( $this, 'render_upsell_modal' ) );
78 }
79
80 /**
81 * Every option key this section owns.
82 *
83 * @return string[]
84 */
85 public static function field_keys() {
86 return array_merge( array( merchant_white_label_switch_key() ), self::shared_keys() );
87 }
88
89 /**
90 * Shared keys for the merchant plugin.
91 *
92 * @return string[]
93 */
94 public static function shared_keys() {
95 $keys = array(
96 'awl_agency_name',
97 'awl_agency_url',
98 'awl_plugin_name_' . self::PRIMARY_SLUG,
99 'awl_plugin_description_' . self::PRIMARY_SLUG,
100 );
101
102 foreach ( self::HIDE_TOGGLES as $toggle ) {
103 $keys[] = self::hide_key( $toggle );
104 }
105
106 return $keys;
107 }
108
109 /**
110 * The field ID one hide toggle is posted under.
111 *
112 * @param string $toggle Toggle name.
113 *
114 * @return string
115 */
116 private static function hide_key( $toggle ) {
117 return 'awl_plugin_hide_' . $toggle . '_' . self::PRIMARY_SLUG;
118 }
119
120 /**
121 * Key prefixes whose single field fans out to a key per plugin slug.
122 *
123 * @return string[]
124 */
125 private static function shared_prefixes() {
126 $prefixes = array( 'awl_plugin_name_', 'awl_plugin_description_' );
127
128 foreach ( self::HIDE_TOGGLES as $toggle ) {
129 $prefixes[] = 'awl_plugin_hide_' . $toggle . '_';
130 }
131
132 return $prefixes;
133 }
134
135 /**
136 * The show-while-the-switch-is-on rule every dependent field carries.
137 *
138 * @return array<string, mixed>
139 */
140 private static function toggled_on() {
141 return array(
142 'relation' => 'AND',
143 'terms' => array(
144 array(
145 'field' => merchant_white_label_switch_key(),
146 'operator' => '===',
147 'value' => true,
148 ),
149 ),
150 );
151 }
152
153 /**
154 * The checkbox label for each hide toggle.
155 *
156 * @return array<string, string>
157 */
158 private static function hide_labels() {
159 return array(
160 'author' => esc_html__( 'Hide Author', 'merchant' ),
161 'version' => esc_html__( 'Hide Version', 'merchant' ),
162 'changelog' => esc_html__( 'Hide Changelog', 'merchant' ),
163 'actions' => esc_html__( 'Hide Plugin Actions', 'merchant' ),
164 );
165 }
166
167 /**
168 * The option keys one field ID is stored under.
169 *
170 * The plugin name, description and hide toggles are one field each in the
171 * UI and apply to every Merchant plugin, so they fan out to a key per slug.
172 *
173 * @param string $field_id Field ID.
174 *
175 * @return string[]
176 */
177 private static function storage_keys( $field_id ) {
178 foreach ( self::shared_prefixes() as $prefix ) {
179 if ( $prefix . self::PRIMARY_SLUG !== $field_id ) {
180 continue;
181 }
182
183 $keys = array();
184
185 foreach ( self::PLUGIN_SLUGS as $slug ) {
186 $keys[] = $prefix . $slug;
187 }
188
189 return $keys;
190 }
191
192 return array( $field_id );
193 }
194
195 /**
196 * Whether the aThemes White Label plugin is white-labelling this site.
197 *
198 * @param mixed $plugin_applying Flag from the caller, if any.
199 *
200 * @return bool
201 */
202 private static function plugin_is_applying( $plugin_applying ) {
203 return is_bool( $plugin_applying ) ? $plugin_applying : merchant_white_label_plugin_is_applying();
204 }
205
206 /**
207 * Whether the aThemes White Label plugin is installed, switched on or not.
208 *
209 * The sync follows the plugin's switch in both directions, so it runs on the
210 * weaker condition than the one the UI reads.
211 *
212 * @param mixed $plugin_active Flag from the caller, if any.
213 *
214 * @return bool
215 */
216 private static function plugin_is_installed( $plugin_active ) {
217 return is_bool( $plugin_active ) ? $plugin_active : merchant_white_label_plugin_available();
218 }
219
220 /**
221 * The switch field, in whichever of its two states applies.
222 *
223 * @param bool $plugin_drives Whether the plugin's switch is what this field reports.
224 *
225 * @return array<string, mixed>
226 */
227 private static function switch_field( $plugin_drives ) {
228 $field = array(
229 'id' => merchant_white_label_switch_key(),
230 'type' => 'switcher',
231 'title' => esc_html__( 'Enable White Label', 'merchant' ),
232 'desc' => esc_html__( 'Replace all Merchant branding and links with your own.', 'merchant' ),
233 'default' => $plugin_drives ? merchant_white_label_plugin_is_switched_on() : merchant_white_label_is_switched_on(),
234 );
235
236 // Switching it off here would change nothing while the plugin is forcing
237 // it on, so the control is shown rather than offered.
238 if ( $plugin_drives ) {
239 $field['locked'] = true;
240 }
241
242 return $field;
243 }
244
245 /**
246 * Field definitions, seeded from the plugin's stored option.
247 *
248 * @param bool|null $plugin_applying Whether the aThemes White Label plugin is applying
249 * White Label. Detected when omitted.
250 *
251 * @return array<int, array<string, mixed>>
252 */
253 public static function get_fields( $plugin_applying = null ) {
254 $stored = merchant_white_label_get_settings();
255 $plugin_applying = self::plugin_is_applying( $plugin_applying );
256 $plugin_drives = $plugin_applying && merchant_white_label_is_available();
257
258 $fields = array();
259
260 $fields[] = self::switch_field( $plugin_drives );
261
262 if ( $plugin_applying ) {
263 $fields[] = array(
264 'id' => 'white_label_plugin_notice',
265 'type' => 'info',
266 'content' => self::plugin_owner_notice( $plugin_drives ),
267 );
268 }
269
270 $fields[] = array(
271 'id' => 'white_label_notice',
272 'type' => 'info',
273 'content' => self::bookmark_notice(),
274 'conditions' => self::toggled_on(),
275 );
276
277 $text_fields = array(
278 'awl_agency_name' => array( esc_html__( 'Your Agency Name', 'merchant' ), 'text' ),
279 'awl_agency_url' => array( esc_html__( 'Your Agency URL', 'merchant' ), 'url' ),
280 );
281
282 foreach ( $text_fields as $key => $field ) {
283 $fields[] = array(
284 'id' => $key,
285 'type' => $field[1],
286 'title' => $field[0],
287 'default' => $stored[ $key ] ?? '',
288 'conditions' => self::toggled_on(),
289 );
290 }
291
292 // Follows the switch as well, or the rule floats above nothing.
293 $fields[] = array(
294 'type' => 'divider',
295 'conditions' => self::toggled_on(),
296 );
297
298 $fields[] = array(
299 'id' => 'awl_plugin_name_' . self::PRIMARY_SLUG,
300 'type' => 'text',
301 'title' => esc_html__( 'Plugin Name', 'merchant' ),
302 'default' => $stored[ 'awl_plugin_name_' . self::PRIMARY_SLUG ] ?? '',
303 'conditions' => self::toggled_on(),
304 );
305 $fields[] = array(
306 'id' => 'awl_plugin_description_' . self::PRIMARY_SLUG,
307 'type' => 'textarea',
308 'title' => esc_html__( 'Plugin Description', 'merchant' ),
309 'default' => $stored[ 'awl_plugin_description_' . self::PRIMARY_SLUG ] ?? '',
310 'conditions' => self::toggled_on(),
311 );
312
313 $first = true;
314 foreach ( self::hide_labels() as $toggle => $label ) {
315 $key = self::hide_key( $toggle );
316
317 $field = array(
318 'id' => $key,
319 'type' => 'checkbox',
320 'label' => $label,
321 'default' => $stored[ $key ] ?? '',
322 'conditions' => self::toggled_on(),
323 );
324
325 // One heading for the whole run, like the framework's other
326 // grouped checkboxes.
327 if ( $first ) {
328 $field['title'] = esc_html__( 'Hide from the plugin row', 'merchant' );
329 $first = false;
330 }
331
332 $fields[] = $field;
333 }
334
335 // Tags every wrapper in this section, so the locked state can turn a
336 // click anywhere on a field into the upgrade modal.
337 foreach ( $fields as $index => $field ) {
338 $fields[ $index ]['class'] = 'merchant-white-label-field';
339 }
340
341 return $fields;
342 }
343
344 /**
345 * Register the section.
346 *
347 * @return void
348 */
349 public function create_settings() {
350 if ( empty( $_POST['merchant_save'] ) && empty( $_POST['merchant_reset'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only branch decision, no data used.
351 self::refresh_cache();
352 }
353
354 Merchant_Admin_Options::create(
355 array(
356 'module' => self::MODULE,
357 'title' => esc_html__( 'White Label', 'merchant' ),
358 'subtitle' => esc_html__( 'Replace Merchant branding with your own.', 'merchant' ),
359 'locked' => ! merchant_white_label_is_available(),
360 'fields' => self::get_fields(),
361 )
362 );
363 }
364
365 /**
366 * Re-seed the render cache from the White Label plugin's option.
367 *
368 * The framework renders from `merchant[white-label]`; that copy is a cache
369 * of `athemes_white_label_settings`, which stays canonical.
370 *
371 * @return void
372 */
373 public static function refresh_cache() {
374 $stored = merchant_white_label_get_settings();
375 $options = get_option( 'merchant', array() );
376 $options = is_array( $options ) ? $options : array();
377 $current = $options[ self::MODULE ] ?? array();
378 $current = is_array( $current ) ? $current : array();
379
380 $switch_key = merchant_white_label_switch_key();
381 $cache = array();
382
383 // The switch is ours alone and is not in the shared option, so it is
384 // carried over rather than reseeded — reading it from there would
385 // silently switch White Label off on every page load.
386 if ( array_key_exists( $switch_key, $current ) ) {
387 $cache[ $switch_key ] = $current[ $switch_key ];
388 }
389
390 foreach ( self::shared_keys() as $key ) {
391 if ( array_key_exists( $key, $stored ) ) {
392 $cache[ $key ] = $stored[ $key ];
393 }
394 }
395
396 if ( $current === $cache ) {
397 return;
398 }
399
400 $options[ self::MODULE ] = $cache;
401 update_option( 'merchant', $options );
402 }
403
404 /**
405 * Print the upgrade modal for sites that aren't entitled.
406 *
407 * @return void
408 */
409 public function render_upsell_modal() {
410 $page = sanitize_text_field( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
411 $section = sanitize_text_field( wp_unslash( $_GET['section'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
412
413 if ( 'merchant' !== $page || 'settings' !== $section || merchant_white_label_is_available() ) {
414 return;
415 }
416
417 require MERCHANT_DIR . 'admin/components/white-label-upsell-modal.php';
418 }
419
420 /**
421 * Carry the aThemes White Label plugin's switch into Merchant's own option.
422 *
423 * Keeps the two in step for as long as the plugin is installed, so removing
424 * it hands White Label over to Merchant's engine with nothing to migrate.
425 * The write goes one way only — the shared option stays the plugin's.
426 *
427 * @param bool|null $plugin_active Whether the aThemes White Label plugin is active.
428 * Detected when omitted.
429 *
430 * @return void
431 */
432 public static function sync_switch_from_plugin( $plugin_active = null ) {
433 if ( ! self::plugin_is_installed( $plugin_active ) || ! merchant_white_label_is_available() ) {
434 return;
435 }
436
437 $value = merchant_white_label_plugin_is_switched_on() ? 1 : 0;
438 $last_seen = get_option( self::LAST_SEEN_OPTION, '' );
439
440 // Following the plugin's switch on every request would overwrite a value
441 // the user set here, so only a change over there is carried across.
442 if ( '' !== $last_seen && (int) $last_seen === $value ) {
443 return;
444 }
445
446 update_option( self::LAST_SEEN_OPTION, $value );
447
448 // First sight of a switched-off plugin has nothing to hand over, and
449 // writing it would switch off a site that had turned this on already.
450 if ( '' === $last_seen && 0 === $value ) {
451 return;
452 }
453
454 self::store_switch( $value );
455 }
456
457 /**
458 * Write the switch into Merchant's own option, leaving the rest alone.
459 *
460 * @param int $value 1 or 0.
461 *
462 * @return void
463 */
464 private static function store_switch( $value ) {
465 $options = get_option( 'merchant', array() );
466 $options = is_array( $options ) ? $options : array();
467 $module = $options[ self::MODULE ] ?? array();
468 $module = is_array( $module ) ? $module : array();
469
470 $module[ merchant_white_label_switch_key() ] = $value;
471 $options[ self::MODULE ] = $module;
472
473 update_option( 'merchant', $options );
474 }
475
476 /**
477 * Say who is white-labelling the site while the aThemes White Label plugin is.
478 *
479 * @param bool $plugin_drives Whether the switch above reports the plugin's own state.
480 *
481 * @return string
482 */
483 public static function plugin_owner_notice( $plugin_drives = true ) {
484 $link = sprintf(
485 '<a href="%s">%s</a>',
486 esc_url( admin_url( 'themes.php?page=athemes-white-label' ) ),
487 esc_html__( 'Change it here', 'merchant' )
488 );
489
490 // An unentitled site still has to know why its branding is masked, even
491 // though Merchant's own switch is not the thing doing it.
492 $text = $plugin_drives
493 /* translators: %s: a link reading "Change it here". */
494 ? esc_html__( 'Switched on by the aThemes White Label plugin. %s.', 'merchant' )
495 /* translators: %s: a link reading "Change it here". */
496 : esc_html__( 'The aThemes White Label plugin is white-labelling this site. %s.', 'merchant' );
497
498 return sprintf( $text, $link );
499 }
500
501 /**
502 * Tell the user how to get back here once the Merchant menu is hidden.
503 *
504 * @return string
505 */
506 public static function bookmark_notice() {
507 $url = add_query_arg(
508 array(
509 'page' => 'merchant',
510 'section' => 'settings',
511 ),
512 admin_url( 'admin.php' )
513 );
514
515 return '<strong>' . esc_html__( 'How do I change these settings after enabling White Label?', 'merchant' ) . '</strong><br>'
516 . esc_html__( 'White Label hides Merchant branding, including its admin menu. Bookmark this address before you save — it is how you get back here:', 'merchant' ) . '<br>'
517 . '<a href="' . esc_url( $url ) . '">' . esc_html( $url ) . '</a>';
518 }
519
520 /**
521 * Copy the saved values into the White Label plugin's own option.
522 *
523 * Merges rather than replaces, so keys written by the aThemes White Label plugin
524 * survive, then refreshes Merchant's cache to match what was stored.
525 *
526 * @param array<string, mixed> $module_options Saved module options.
527 *
528 * @return void
529 */
530 public static function mirror_to_plugin_option( $module_options ) {
531 $stored = merchant_white_label_get_settings();
532 $shared = self::shared_keys();
533 $cache = array();
534
535 foreach ( self::field_keys() as $key ) {
536 if ( ! array_key_exists( $key, $module_options ) ) {
537 continue;
538 }
539
540 $value = self::sanitize_value( $key, $module_options[ $key ] );
541 $cache[ $key ] = $value;
542
543 // The switch is cached for our own screen and engine only. Writing
544 // it to the shared option would turn White Label on for every other
545 // aThemes product on the site.
546 if ( ! in_array( $key, $shared, true ) ) {
547 continue;
548 }
549
550 foreach ( self::storage_keys( $key ) as $storage_key ) {
551 $stored[ $storage_key ] = $value;
552 }
553 }
554
555 update_option( 'athemes_white_label_settings', $stored );
556
557 // Keep the render cache in step with what was just stored, otherwise
558 // the render that follows this save shows the pre-save values.
559 $options = get_option( 'merchant', array() );
560 $options = is_array( $options ) ? $options : array();
561
562 $options[ self::MODULE ] = $cache;
563 update_option( 'merchant', $options );
564 }
565
566 /**
567 * Sanitize one value for storage.
568 *
569 * The plugin's own sanitize callback only runs on Settings API saves, so
570 * this write path has to do its own.
571 *
572 * @param string $key Option key.
573 * @param mixed $value Raw value.
574 *
575 * @return mixed
576 */
577 private static function sanitize_value( $key, $value ) {
578 if ( merchant_white_label_switch_key() === $key || 0 === strpos( $key, 'awl_plugin_hide_' ) ) {
579 return empty( $value ) ? 0 : 1;
580 }
581
582 if ( 'awl_agency_url' === $key ) {
583 return sanitize_url( $value );
584 }
585
586 if ( 0 === strpos( $key, 'awl_plugin_description_' ) ) {
587 return sanitize_textarea_field( $value );
588 }
589
590 return sanitize_text_field( $value );
591 }
592 }
593
594 new Merchant_White_Label_Settings();
595 }
596