PluginProbe
Download Manager Addons for Elementor / trunk
Download Manager Addons for Elementor vtrunk
trunk 1.0.5 1.2.3 1.2.5 1.3.0
wpdm-elementor / src / widgets / BaseWidget.php

BaseWidget.php in Download Manager Addons for Elementor trunk, at src/widgets/BaseWidget.php

522 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Base widget class for all WPDM Elementor widgets.
4 *
5 * @package WPDM\Elementor\Widgets
6 * @since 1.0.0
7 */
8
9 namespace WPDM\Elementor\Widgets;
10
11 use Elementor\Widget_Base;
12 use Elementor\Controls_Manager;
13 use Elementor\Group_Control_Typography;
14 use Elementor\Group_Control_Border;
15 use Elementor\Group_Control_Box_Shadow;
16
17 /**
18 * Abstract base class for all WPDM Elementor widgets.
19 *
20 * Provides common functionality, helper methods, and reduces code duplication
21 * across all WPDM widgets. All widgets should extend this class.
22 *
23 * @since 1.0.0
24 * @package WPDM\Elementor\Widgets
25 */
26 abstract class BaseWidget extends Widget_Base
27 {
28 /**
29 * Get widget categories.
30 *
31 * All WPDM widgets belong to the 'wpdm' category.
32 *
33 * @since 1.0.0
34 * @return array Widget categories.
35 */
36 public function get_categories(): array
37 {
38 return ['wpdm'];
39 }
40
41 /**
42 * Get widget keywords for search.
43 *
44 * Override in child classes to add specific keywords.
45 *
46 * @since 1.3.0
47 * @return array Widget keywords.
48 */
49 public function get_keywords(): array
50 {
51 return ['wpdm', 'download', 'manager'];
52 }
53
54 /**
55 * Get clean settings by specified keys only.
56 *
57 * Replaces the fragile array_slice approach with explicit key extraction.
58 * This ensures only expected settings are passed to WPDM shortcodes.
59 *
60 * @since 1.0.0
61 * @param array $keys Array of setting keys to extract.
62 * @return array Filtered settings array.
63 */
64 protected function getCleanSettings(array $keys): array
65 {
66 $all_settings = $this->get_settings_for_display();
67 $clean = [];
68
69 foreach ($keys as $key) {
70 if (array_key_exists($key, $all_settings)) {
71 $clean[$key] = $all_settings[$key];
72 }
73 }
74
75 return $clean;
76 }
77
78 /**
79 * Convert array setting to comma-separated string.
80 *
81 * @since 1.0.0
82 * @param array $settings Settings array.
83 * @param string $key Key of the setting to convert.
84 * @return array Modified settings array.
85 */
86 protected function arrayToComma(array $settings, string $key): array
87 {
88 if (isset($settings[$key]) && is_array($settings[$key])) {
89 $settings[$key] = implode(',', array_map('sanitize_text_field', $settings[$key]));
90 }
91 return $settings;
92 }
93
94 /**
95 * Convert multiple array settings to comma-separated strings.
96 *
97 * @since 1.0.0
98 * @param array $settings Settings array.
99 * @param array $keys Keys to convert.
100 * @return array Modified settings array.
101 */
102 protected function arraysToComma(array $settings, array $keys): array
103 {
104 foreach ($keys as $key) {
105 $settings = $this->arrayToComma($settings, $key);
106 }
107 return $settings;
108 }
109
110 /**
111 * Sanitize settings based on type definitions.
112 *
113 * Supported types:
114 * - 'int': Integer value (absint)
115 * - 'bool': Boolean value
116 * - 'text': Sanitized text field
117 * - 'url': Sanitized URL
118 * - 'html': Allowed HTML (wp_kses_post)
119 * - 'css_class': CSS class name
120 * - 'css_style': Inline CSS styles
121 * - 'slug': URL slug
122 * - 'order': ASC or DESC
123 * - 'orderby': date, title, modified, or rand
124 *
125 * @since 1.0.0
126 * @param array $settings Settings array.
127 * @param array $sanitizers Associative array of key => type.
128 * @return array Sanitized settings array.
129 */
130 protected function sanitizeSettings(array $settings, array $sanitizers): array
131 {
132 foreach ($sanitizers as $key => $type) {
133 if (!isset($settings[$key])) {
134 continue;
135 }
136
137 $value = $settings[$key];
138
139 switch ($type) {
140 case 'int':
141 $settings[$key] = absint($value);
142 break;
143
144 case 'bool':
145 $settings[$key] = (bool) $value;
146 break;
147
148 case 'text':
149 $settings[$key] = sanitize_text_field($value);
150 break;
151
152 case 'url':
153 $settings[$key] = esc_url_raw($value);
154 break;
155
156 case 'html':
157 $settings[$key] = wp_kses_post($value);
158 break;
159
160 case 'css_class':
161 $settings[$key] = sanitize_html_class($value);
162 break;
163
164 case 'css_style':
165 $settings[$key] = $this->sanitizeCssStyle($value);
166 break;
167
168 case 'slug':
169 $settings[$key] = sanitize_title($value);
170 break;
171
172 case 'order':
173 $settings[$key] = in_array(strtoupper($value), ['ASC', 'DESC'], true)
174 ? strtoupper($value)
175 : 'DESC';
176 break;
177
178 case 'orderby':
179 $allowed = ['date', 'title', 'modified', 'rand', 'menu_order'];
180 $settings[$key] = in_array($value, $allowed, true) ? $value : 'date';
181 break;
182 }
183 }
184
185 return $settings;
186 }
187
188 /**
189 * Sanitize inline CSS style attribute.
190 *
191 * Removes potentially dangerous CSS functions and HTML tags.
192 *
193 * @since 1.0.0
194 * @param string $style Raw CSS style string.
195 * @return string Sanitized CSS style.
196 */
197 protected function sanitizeCssStyle(string $style): string
198 {
199 // Remove any HTML tags
200 $style = wp_strip_all_tags($style);
201
202 // Remove potentially dangerous CSS functions
203 $dangerous = ['expression', 'javascript:', 'behavior:', 'vbscript:', 'url('];
204 foreach ($dangerous as $pattern) {
205 if (stripos($style, $pattern) !== false) {
206 return '';
207 }
208 }
209
210 return esc_attr($style);
211 }
212
213 /**
214 * Wrap output in a container div.
215 *
216 * @since 1.0.0
217 * @param string $content HTML content to wrap.
218 * @param string $class Additional CSS class (optional).
219 * @return string Wrapped content.
220 */
221 protected function wrapOutput(string $content, string $class = ''): string
222 {
223 $classes = 'wpdm-elementor-widget';
224 if (!empty($class)) {
225 $classes .= ' ' . sanitize_html_class($class);
226 }
227
228 return sprintf('<div class="%s">%s</div>', esc_attr($classes), $content);
229 }
230
231 /**
232 * Convert boolean-like settings (0/1 strings) to actual booleans.
233 *
234 * @since 1.0.0
235 * @param array $settings Settings array.
236 * @param array $keys Keys to convert.
237 * @return array Modified settings array.
238 */
239 protected function toBooleans(array $settings, array $keys): array
240 {
241 foreach ($keys as $key) {
242 if (isset($settings[$key])) {
243 $settings[$key] = (bool) $settings[$key];
244 }
245 }
246 return $settings;
247 }
248
249 /**
250 * Helper to get WPDM category terms for controls.
251 *
252 * @since 1.0.0
253 * @return array Category slug => name pairs.
254 */
255 protected function getCategoryOptions(): array
256 {
257 return get_wpdmcategory_terms();
258 }
259
260 /**
261 * Helper to get link template options for controls.
262 *
263 * @since 1.0.0
264 * @return array Template options.
265 */
266 protected function getLinkTemplateOptions(): array
267 {
268 return get_elementor_link_templates();
269 }
270
271 /**
272 * Helper to get WPDM tag terms for controls.
273 *
274 * @since 1.0.0
275 * @return array Tag slug => name pairs.
276 */
277 protected function getTagOptions(): array
278 {
279 if (function_exists('get_wpdm_tag_terms')) {
280 return get_wpdm_tag_terms();
281 }
282
283 $tags = get_terms(['taxonomy' => 'wpdmtag', 'hide_empty' => false]);
284 $options = [];
285
286 if (is_array($tags) && !is_wp_error($tags)) {
287 foreach ($tags as $tag) {
288 $options[$tag->slug] = $tag->name;
289 }
290 }
291
292 return $options;
293 }
294
295 /**
296 * Get the SELECT2 AJAX configuration for package search.
297 *
298 * @since 1.0.0
299 * @return array SELECT2 options array.
300 */
301 protected function getPackageSearchConfig(): array
302 {
303 return [
304 'placeholder' => __('Type package title', WPDM_ELEMENTOR),
305 'ajax' => [
306 'url' => get_rest_url(null, 'wpdm-elementor/v1/search-packages'),
307 'dataType' => 'json',
308 'delay' => 250,
309 ],
310 'minimumInputLength' => 2,
311 ];
312 }
313
314 /**
315 * Register container style controls section.
316 *
317 * Adds background, border, and shadow controls for the widget container.
318 *
319 * @since 1.3.0
320 * @param string $section_id Section ID (default: 'style_container').
321 * @param string $section_label Section label.
322 * @return void
323 */
324 protected function registerContainerStyleControls(
325 string $section_id = 'style_container',
326 string $section_label = ''
327 ): void {
328 if (empty($section_label)) {
329 $section_label = __('Container Style', WPDM_ELEMENTOR);
330 }
331
332 $this->start_controls_section(
333 $section_id,
334 [
335 'label' => $section_label,
336 'tab' => Controls_Manager::TAB_STYLE,
337 ]
338 );
339
340 $this->add_control(
341 'container_background',
342 [
343 'label' => __('Background Color', WPDM_ELEMENTOR),
344 'type' => Controls_Manager::COLOR,
345 'selectors' => [
346 '{{WRAPPER}} .wpdm-elementor-widget' => 'background-color: {{VALUE}};',
347 ],
348 ]
349 );
350
351 $this->add_responsive_control(
352 'container_padding',
353 [
354 'label' => __('Padding', WPDM_ELEMENTOR),
355 'type' => Controls_Manager::DIMENSIONS,
356 'size_units' => ['px', 'em', '%'],
357 'selectors' => [
358 '{{WRAPPER}} .wpdm-elementor-widget' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
359 ],
360 ]
361 );
362
363 $this->add_responsive_control(
364 'container_margin',
365 [
366 'label' => __('Margin', WPDM_ELEMENTOR),
367 'type' => Controls_Manager::DIMENSIONS,
368 'size_units' => ['px', 'em', '%'],
369 'selectors' => [
370 '{{WRAPPER}} .wpdm-elementor-widget' => 'margin: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
371 ],
372 ]
373 );
374
375 $this->add_group_control(
376 Group_Control_Border::get_type(),
377 [
378 'name' => 'container_border',
379 'selector' => '{{WRAPPER}} .wpdm-elementor-widget',
380 ]
381 );
382
383 $this->add_responsive_control(
384 'container_border_radius',
385 [
386 'label' => __('Border Radius', WPDM_ELEMENTOR),
387 'type' => Controls_Manager::DIMENSIONS,
388 'size_units' => ['px', '%'],
389 'selectors' => [
390 '{{WRAPPER}} .wpdm-elementor-widget' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
391 ],
392 ]
393 );
394
395 $this->add_group_control(
396 Group_Control_Box_Shadow::get_type(),
397 [
398 'name' => 'container_box_shadow',
399 'selector' => '{{WRAPPER}} .wpdm-elementor-widget',
400 ]
401 );
402
403 $this->end_controls_section();
404 }
405
406 /**
407 * Register typography style controls section.
408 *
409 * Adds typography controls for title and content.
410 *
411 * @since 1.3.0
412 * @param string $selector CSS selector for the typography target.
413 * @return void
414 */
415 protected function registerTypographyControls(string $selector = '.wpdm-elementor-widget'): void
416 {
417 $this->start_controls_section(
418 'style_typography',
419 [
420 'label' => __('Typography', WPDM_ELEMENTOR),
421 'tab' => Controls_Manager::TAB_STYLE,
422 ]
423 );
424
425 $this->add_control(
426 'title_color',
427 [
428 'label' => __('Title Color', WPDM_ELEMENTOR),
429 'type' => Controls_Manager::COLOR,
430 'selectors' => [
431 '{{WRAPPER}} ' . $selector . ' h1, {{WRAPPER}} ' . $selector . ' h2, {{WRAPPER}} ' . $selector . ' h3, {{WRAPPER}} ' . $selector . ' h4, {{WRAPPER}} ' . $selector . ' h5, {{WRAPPER}} ' . $selector . ' .title' => 'color: {{VALUE}};',
432 ],
433 ]
434 );
435
436 $this->add_group_control(
437 Group_Control_Typography::get_type(),
438 [
439 'name' => 'title_typography',
440 'label' => __('Title Typography', WPDM_ELEMENTOR),
441 'selector' => '{{WRAPPER}} ' . $selector . ' h1, {{WRAPPER}} ' . $selector . ' h2, {{WRAPPER}} ' . $selector . ' h3, {{WRAPPER}} ' . $selector . ' h4, {{WRAPPER}} ' . $selector . ' h5, {{WRAPPER}} ' . $selector . ' .title',
442 ]
443 );
444
445 $this->add_control(
446 'text_color',
447 [
448 'label' => __('Text Color', WPDM_ELEMENTOR),
449 'type' => Controls_Manager::COLOR,
450 'separator' => 'before',
451 'selectors' => [
452 '{{WRAPPER}} ' . $selector => 'color: {{VALUE}};',
453 ],
454 ]
455 );
456
457 $this->add_group_control(
458 Group_Control_Typography::get_type(),
459 [
460 'name' => 'text_typography',
461 'label' => __('Text Typography', WPDM_ELEMENTOR),
462 'selector' => '{{WRAPPER}} ' . $selector,
463 ]
464 );
465
466 $this->add_control(
467 'link_color',
468 [
469 'label' => __('Link Color', WPDM_ELEMENTOR),
470 'type' => Controls_Manager::COLOR,
471 'separator' => 'before',
472 'selectors' => [
473 '{{WRAPPER}} ' . $selector . ' a' => 'color: {{VALUE}};',
474 ],
475 ]
476 );
477
478 $this->add_control(
479 'link_hover_color',
480 [
481 'label' => __('Link Hover Color', WPDM_ELEMENTOR),
482 'type' => Controls_Manager::COLOR,
483 'selectors' => [
484 '{{WRAPPER}} ' . $selector . ' a:hover' => 'color: {{VALUE}};',
485 ],
486 ]
487 );
488
489 $this->end_controls_section();
490 }
491
492 /**
493 * Check if we are in Elementor editor mode.
494 *
495 * Useful for showing placeholder content in the editor.
496 *
497 * @since 1.3.0
498 * @return bool True if in editor mode.
499 */
500 protected function isEditorMode(): bool
501 {
502 return \Elementor\Plugin::$instance->editor->is_edit_mode();
503 }
504
505 /**
506 * Render placeholder for empty content in editor.
507 *
508 * @since 1.3.0
509 * @param string $message Message to display.
510 * @return void
511 */
512 protected function renderEditorPlaceholder(string $message): void
513 {
514 if ($this->isEditorMode()) {
515 printf(
516 '<div class="wpdm-elementor-placeholder" style="padding: 20px; background: #f5f5f5; border: 1px dashed #ccc; text-align: center;">%s</div>',
517 esc_html($message)
518 );
519 }
520 }
521 }
522