PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.9
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / admin / widget-builder / class-control-manager.php

class-control-manager.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.9, at inc/admin/widget-builder/class-control-manager.php

463 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace MasterAddons\Inc\Admin\WidgetBuilder;
3
4 use MasterAddons\Inc\Classes\Helper;
5
6 defined('ABSPATH') || exit;
7
8 /**
9 * Control Manager
10 * Loads and manages all control type builders
11 */
12 class Control_Manager {
13
14 private static $instance = null;
15 private $controls = [];
16
17 public static function get_instance() {
18 if (self::$instance === null) {
19 self::$instance = new self();
20 }
21 return self::$instance;
22 }
23
24 public function __construct() {
25 $this->load_controls();
26 }
27
28 /**
29 * Load all control classes
30 */
31 private function load_controls() {
32 // Load base class first
33 require_once __DIR__ . '/controls/class-control-base.php';
34
35 // Load FREE control type classes only
36 $control_files = [
37 'color',
38 'heading',
39 'hidden',
40 'text',
41 'textarea',
42 'url',
43 'number',
44 'choose',
45 'divider',
46 'font',
47 'media',
48 'select',
49 'switcher',
50 'wysiwyg',
51 ];
52
53 foreach ($control_files as $file) {
54 $file_path = __DIR__ . '/controls/' . $file . '.php';
55 if (file_exists($file_path)) {
56 require_once $file_path;
57 }
58 }
59
60 // Allow Pro to load additional controls
61 do_action('jltma_widget_builder_load_controls');
62 }
63
64 /**
65 * Get control builder instance by type
66 *
67 * @param string $type Control type (TEXT, TEXTAREA, etc.) - can be uppercase or lowercase
68 * @return Controls\Control_Base|null
69 */
70 public function get_control($type) {
71 // Convert to uppercase for consistency (React UI sends lowercase, but we need uppercase for mapping)
72 $type = strtoupper($type);
73
74 // Return cached instance if exists
75 if (isset($this->controls[$type])) {
76 return $this->controls[$type];
77 }
78
79 // Map control types to classes (FREE controls only)
80 $class_map = [
81 'COLOR' => 'Color',
82 'HEADING' => 'Heading',
83 'HIDDEN' => 'Hidden',
84 'TEXT' => 'Text',
85 'TEXTAREA' => 'Textarea',
86 'URL' => 'Url',
87 'NUMBER' => 'Number',
88 'CHOOSE' => 'Choose',
89 'DIVIDER' => 'Divider',
90 'FONT' => 'Font',
91 'MEDIA' => 'Media',
92 'SELECT' => 'Select',
93 'SWITCHER' => 'Switcher',
94 'WYSIWYG' => 'Wysiwyg',
95 ];
96
97 // Allow Pro to add additional control class mappings
98 $class_map = apply_filters('jltma_widget_builder_control_class_map', $class_map);
99
100 if (!isset($class_map[$type])) {
101 return null;
102 }
103
104 // Check if class_map value is a full class name (from Pro) or just class name (free)
105 if (strpos($class_map[$type], '\\') !== false) {
106 $class_name = $class_map[$type];
107 } else {
108 $class_name = '\\MasterAddons\\Inc\\Admin\\WidgetBuilder\\Controls\\' . $class_map[$type];
109 }
110
111 if (!class_exists($class_name)) {
112 return null;
113 }
114
115 // Create and cache instance
116 $this->controls[$type] = new $class_name();
117
118 return $this->controls[$type];
119 }
120
121 /**
122 * Control types that are premium and not unlocked on this site.
123 *
124 * The catalog is declared once by Widget_Admin and Pro flips each entry's
125 * isPro flag off through `jltma_widget_builder_pro_controls` when a licence
126 * is active — the same list that locks the builder UI, reused here so the
127 * editor cannot be a way around it.
128 *
129 * @return array Uppercase control types, as a lookup map.
130 */
131 public static function get_locked_types() {
132 static $locked = null;
133
134 if (null !== $locked) {
135 return $locked;
136 }
137
138 $locked = [];
139 $catalog = apply_filters(
140 'jltma_widget_builder_pro_controls',
141 class_exists('\\MasterAddons\\Inc\\Admin\\WidgetBuilder\\Widget_Admin')
142 ? Widget_Admin::get_pro_controls_catalog()
143 : []
144 );
145
146 if (!is_array($catalog)) {
147 return $locked;
148 }
149
150 foreach ($catalog as $control) {
151 if (is_array($control) && !empty($control['isPro']) && !empty($control['type'])) {
152 $locked[strtoupper($control['type'])] = true;
153 }
154 }
155
156 return $locked;
157 }
158
159 /**
160 * Is this control type premium and still locked?
161 *
162 * @param string $type Control type.
163 * @return bool
164 */
165 public function is_locked_type($type) {
166 $type = strtoupper((string) $type);
167 $locked = self::get_locked_types();
168
169 if (isset($locked[$type])) {
170 return true;
171 }
172
173 // Group controls arrive as GROUP_CONTROL_TYPOGRAPHY etc.
174 if (0 === strpos($type, 'GROUP_CONTROL_')) {
175 return isset($locked[substr($type, strlen('GROUP_CONTROL_'))]);
176 }
177
178 return false;
179 }
180
181 /**
182 * Config for a premium control on a site without a licence.
183 *
184 * Registers no editable control. The value is carried by a HIDDEN control
185 * under the key the control would normally own, so anything already saved
186 * — by a Pro site, or by an imported widget — still reaches the frontend
187 * and is preserved on save. A RAW_HTML notice explains the lock.
188 *
189 * @param string $control_key Control key.
190 * @param array $field Field configuration.
191 * @param string $type Control type.
192 * @param bool $with_notice Whether to include the panel notice.
193 * @return array
194 */
195 public function build_locked_control_config($control_key, $field, $type, $with_notice = true) {
196 $label = !empty($field['label']) ? $field['label'] : 'Control';
197
198 $args = [
199 // translators: dynamic user-defined control label.
200 'label' => esc_html($label),
201 'type' => class_exists('\\Elementor\\Controls_Manager') ? \Elementor\Controls_Manager::HIDDEN : 'hidden',
202 ];
203
204 if (isset($field['default'])) {
205 $args['default'] = $field['default'];
206 }
207
208 $descriptor = [
209 'key' => $control_key,
210 'responsive' => false,
211 'method' => 'locked',
212 'args' => $args,
213 'notice' => null,
214 ];
215
216 if ($with_notice) {
217 $descriptor['notice'] = $this->build_locked_badge_config($control_key, $field, $label);
218 }
219
220 return $descriptor;
221 }
222
223 /**
224 * The locked "(Pro)" badge shown in place of a premium control.
225 *
226 * Same shape the rest of the plugin uses for locked controls (see the Popup
227 * Builder's Pro options): a CHOOSE control whose only option is a lock icon,
228 * so the row reads as a disabled button, with the upgrade link underneath.
229 *
230 * @param string $control_key Control key the real control would own.
231 * @param array $field Field configuration.
232 * @param string $label Control label.
233 * @return array
234 */
235 private function build_locked_badge_config($control_key, $field, $label) {
236 return [
237 'key' => $control_key . '_jltma_pro_notice',
238 'responsive' => !empty($field['responsive']),
239 'args' => [
240 'label' => sprintf(
241 /* translators: %s: dynamic user-defined control label. */
242 esc_html__('%s (Pro)', 'master-addons'),
243 $label
244 ),
245 'type' => class_exists('\\Elementor\\Controls_Manager') ? \Elementor\Controls_Manager::CHOOSE : 'choose',
246 'options' => [
247 '1' => [
248 'title' => '',
249 'icon' => 'eicon-lock',
250 ],
251 ],
252 'default' => '1',
253 'description' => Helper::unlock_pro_feature(),
254 ],
255 ];
256 }
257
258 /**
259 * Build control output
260 *
261 * @param string $control_key Control key
262 * @param array $field Field configuration
263 * @param string $type Control type
264 * @return string
265 */
266 public function build_control($control_key, $field, $type) {
267 $control = $this->get_control($type);
268
269 if (!$control) {
270 // Fallback for unknown control types
271 return $this->build_fallback_control($control_key, $field, $type);
272 }
273
274 return $control->build($control_key, $field);
275 }
276
277 /**
278 * Build control config ARRAY (runtime equivalent of build_control()).
279 * Consumed by Dynamic_Widget::register_controls() so no PHP file is generated.
280 *
281 * @param string $control_key Control key
282 * @param array $field Field configuration
283 * @param string $type Control type
284 * @return array ['key' => string, 'responsive' => bool, 'args' => array]
285 */
286 public function build_control_config($control_key, $field, $type) {
287 // Premium types never reach the fallback: without this the fallback
288 // rebuilds them as native Elementor controls, handing free sites a fully
289 // editable Typography/Background/... panel for any imported widget.
290 if ($this->is_locked_type($type)) {
291 return $this->build_locked_control_config($control_key, $field, $type);
292 }
293
294 $control = $this->get_control($type);
295
296 if (!$control || !method_exists($control, 'get_config')) {
297 return $this->build_fallback_control_config($control_key, $field, $type);
298 }
299
300 return $control->get_config($control_key, $field);
301 }
302
303 /**
304 * Fallback control config for unknown types (array equivalent of build_fallback_control()).
305 *
306 * @param string $control_key Control key
307 * @param array $field Field configuration
308 * @param string $type Control type
309 * @return array
310 */
311 private function build_fallback_control_config($control_key, $field, $type) {
312 $type_const = strtoupper(preg_replace('/[^A-Za-z0-9_]/', '', (string) $type));
313 if ('' === $type_const) {
314 $type_const = 'TEXT';
315 }
316
317 $const = '\\Elementor\\Controls_Manager::' . $type_const;
318 $args = [
319 // translators: dynamic user-defined control label.
320 'label' => esc_html(!empty($field['label']) ? $field['label'] : 'Control'),
321 'type' => defined($const) ? constant($const) : 'text',
322 ];
323
324 // REPEATER must always carry a 'fields' array.
325 if ('REPEATER' === $type_const) {
326 $args['fields'] = [];
327 }
328
329 if (isset($field['default'])) {
330 $default = $this->normalize_fallback_default($field['default'], $args['type']);
331 if (null !== $default) {
332 $args['default'] = $default;
333 }
334 }
335
336 return [
337 'key' => $control_key,
338 'responsive' => !empty($field['responsive']),
339 'args' => $args,
340 ];
341 }
342
343 /**
344 * Reconcile a stored default with the shape Elementor expects for the control type.
345 *
346 * Data controls such as MEDIA, ICONS, GALLERY or DIMENSIONS declare an ARRAY
347 * default. Elementor merges our value into it (array_merge), so handing it the
348 * scalar '' the builder stores for an untouched field raises a TypeError and
349 * fatals the editor's controls-config ajax request. Wrap a usable scalar into
350 * the array shape when we can infer it, otherwise drop the key and let
351 * Elementor fall back to its own default.
352 *
353 * @param mixed $default Stored default value.
354 * @param string $control_type Resolved Elementor control type.
355 * @return mixed|null Normalized default, or null when it must be omitted.
356 */
357 private function normalize_fallback_default($default, $control_type) {
358 if (is_array($default) || !class_exists('\\Elementor\\Plugin')) {
359 return $default;
360 }
361
362 $elementor = \Elementor\Plugin::instance();
363 if (empty($elementor->controls_manager)) {
364 return $default;
365 }
366
367 $instance = $elementor->controls_manager->get_control($control_type);
368 if (!$instance || !($instance instanceof \Elementor\Base_Data_Control)) {
369 return $default;
370 }
371
372 $control_default = $instance->get_default_value();
373 if (!is_array($control_default)) {
374 return $default;
375 }
376
377 $value = is_scalar($default) ? (string) $default : '';
378 if ('' === $value) {
379 return null;
380 }
381
382 // Single-value shapes we can safely infer (MEDIA/IMAGE -> url, ICONS -> value).
383 foreach (['url', 'value'] as $key) {
384 if (array_key_exists($key, $control_default)) {
385 return array_merge($control_default, [$key => $value]);
386 }
387 }
388
389 return null;
390 }
391
392 /**
393 * Build fallback control for unknown types
394 *
395 * @param string $control_key Control key
396 * @param array $field Field configuration
397 * @param string $type Control type
398 * @return string
399 */
400 private function build_fallback_control($control_key, $field, $type) {
401 $label = !empty($field['label']) ? esc_js($field['label']) : 'Control';
402
403 // Elementor control constants are upper-case (e.g. REPEATER, SELECT). Normalise
404 // the type to a safe constant name so the generated PHP is always valid.
405 $type_const = strtoupper(preg_replace('/[^A-Za-z0-9_]/', '', (string) $type));
406 if ('' === $type_const) {
407 $type_const = 'TEXT';
408 }
409
410 $content = "\t\t\$this->add_control(\n";
411 $content .= "\t\t\t'{$control_key}',\n";
412 $content .= "\t\t\t[\n";
413 $content .= "\t\t\t\t'label' => esc_html__('{$label}', 'master-addons'),\n";
414 $content .= "\t\t\t\t'type' => Controls_Manager::{$type_const},\n";
415
416 // REPEATER must always carry a 'fields' array, otherwise Elementor's
417 // sanitize_settings() throws a TypeError when iterating null fields.
418 if ('REPEATER' === $type_const) {
419 $content .= "\t\t\t\t'fields' => array(),\n";
420 }
421
422 if (isset($field['default'])) {
423 $const_name = '\\Elementor\\Controls_Manager::' . $type_const;
424 $native_type = defined($const_name) ? constant($const_name) : 'text';
425 $normalized = $this->normalize_fallback_default($field['default'], $native_type);
426 if (null !== $normalized) {
427 $default = $this->export_default_value($normalized);
428 $content .= "\t\t\t\t'default' => {$default},\n";
429 }
430 }
431
432 $content .= "\t\t\t]\n";
433 $content .= "\t\t);\n\n";
434
435 return $content;
436 }
437
438 /**
439 * Convert a control default value into a valid PHP literal for the generated file.
440 * Arrays (e.g. repeater/group-control defaults) must never be interpolated directly,
441 * which would emit the literal token "Array" and produce a fatal parse error.
442 *
443 * @param mixed $value
444 * @return string
445 */
446 private function export_default_value($value) {
447 if (is_array($value)) {
448 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- generating PHP source, not debug output
449 return var_export($value, true);
450 }
451 if (is_bool($value)) {
452 return $value ? 'true' : 'false';
453 }
454 if (is_int($value) || is_float($value)) {
455 return (string) $value;
456 }
457 if (null === $value) {
458 return "''";
459 }
460 return "'" . esc_js((string) $value) . "'";
461 }
462 }
463