PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / features / Protected_Content / Protected_Content.php

Protected_Content.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.49, at includes/features/Protected_Content/Protected_Content.php

1,476 lines 50.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Protected Content feature.
4 *
5 * Adds protection controls and runtime logic for Elementor elements.
6 * Supports role-based access, password protection, conditions, and content locker.
7 *
8 * @package King_Addons
9 */
10
11 namespace King_Addons;
12
13 use Elementor\Controls_Manager;
14 use Elementor\Element_Base;
15 use Elementor\Plugin;
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Protected Content class.
23 */
24 class Protected_Content
25 {
26 /**
27 * Templates cache.
28 *
29 * @var array<int,string>|null
30 */
31 private static ?array $templates_cache = null;
32
33 /**
34 * Roles cache.
35 *
36 * @var array<string,string>|null
37 */
38 private static ?array $roles_cache = null;
39
40 /**
41 * Constructor.
42 */
43 public function __construct()
44 {
45 // Styles and scripts.
46 add_action('elementor/preview/enqueue_styles', [$this, 'enqueue_styles'], 1);
47 add_action('elementor/frontend/after_enqueue_styles', [$this, 'enqueue_styles'], 1);
48 add_action('elementor/preview/enqueue_scripts', [$this, 'enqueue_preview_script'], 1);
49
50 // Controls for widgets / columns / sections / containers.
51 add_action('elementor/element/common/_section_style/after_section_end', [$this, 'register_controls'], 10, 2);
52 add_action('elementor/element/section/section_advanced/after_section_end', [$this, 'register_controls'], 10, 2);
53 add_action('elementor/element/container/section_layout/after_section_end', [$this, 'register_controls'], 10, 2);
54 add_action('elementor/element/column/section_advanced/after_section_end', [$this, 'register_controls'], 10, 2);
55
56 // Runtime check.
57 add_action('elementor/frontend/widget/before_render', [$this, 'before_render_element']);
58 add_action('elementor/frontend/section/before_render', [$this, 'before_render_element']);
59 add_action('elementor/frontend/container/before_render', [$this, 'before_render_element']);
60 add_action('elementor/frontend/column/before_render', [$this, 'before_render_element']);
61
62 // Locker helper.
63 add_action('king_addons_locker_unlock', [$this, 'set_locker_cookie']);
64
65 // Password AJAX handler.
66 add_action('wp_ajax_king_addons_verify_password', [$this, 'ajax_verify_password']);
67 add_action('wp_ajax_nopriv_king_addons_verify_password', [$this, 'ajax_verify_password']);
68
69 // Enqueue password form script.
70 add_action('wp_enqueue_scripts', [$this, 'enqueue_password_form_script']);
71 }
72
73 /**
74 * Enqueue styles.
75 *
76 * @return void
77 */
78 public function enqueue_styles(): void
79 {
80 wp_enqueue_style(
81 KING_ADDONS_ASSETS_UNIQUE_KEY . '-protected-content-style',
82 KING_ADDONS_URL . 'includes/features/Protected_Content/style.css',
83 [],
84 KING_ADDONS_VERSION
85 );
86 }
87
88 /**
89 * Enqueue preview script in editor.
90 *
91 * @return void
92 */
93 public function enqueue_preview_script(): void
94 {
95 wp_enqueue_script(
96 KING_ADDONS_ASSETS_UNIQUE_KEY . '-protected-content-preview-handler',
97 KING_ADDONS_URL . 'includes/features/Protected_Content/preview-handler.js',
98 ['jquery'],
99 KING_ADDONS_VERSION,
100 true
101 );
102 }
103
104 /**
105 * Enqueue password form script on frontend.
106 *
107 * @return void
108 */
109 public function enqueue_password_form_script(): void
110 {
111 if ($this->is_editor_or_preview()) {
112 return;
113 }
114
115 wp_enqueue_script(
116 KING_ADDONS_ASSETS_UNIQUE_KEY . '-protected-content-password',
117 KING_ADDONS_URL . 'includes/features/Protected_Content/password-form.js',
118 ['jquery'],
119 KING_ADDONS_VERSION,
120 true
121 );
122
123 wp_localize_script(
124 KING_ADDONS_ASSETS_UNIQUE_KEY . '-protected-content-password',
125 'kingAddonsProtectedContent',
126 [
127 'ajaxUrl' => admin_url('admin-ajax.php'),
128 'nonce' => wp_create_nonce('king_addons_protected_content'),
129 ]
130 );
131 }
132
133 /**
134 * Register protection controls on supported elements.
135 *
136 * @param Element_Base $element Element.
137 * @param array<mixed> $args Args.
138 *
139 * @return void
140 */
141 public function register_controls(Element_Base $element, $args): void
142 {
143 $element->start_controls_section(
144 'king_addons_protected_content_section',
145 [
146 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Protection', 'king-addons'),
147 'tab' => Controls_Manager::TAB_ADVANCED,
148 ]
149 );
150
151 // =============================================
152 // MAIN SETTINGS
153 // =============================================
154
155 $element->add_control(
156 'protected_content_enable',
157 [
158 'label' => esc_html__('Enable Protection', 'king-addons'),
159 'type' => Controls_Manager::SWITCHER,
160 'label_on' => esc_html__('Yes', 'king-addons'),
161 'label_off' => esc_html__('No', 'king-addons'),
162 'return_value' => 'yes',
163 'default' => '',
164 ]
165 );
166
167 $element->add_control(
168 'protected_content_mode',
169 [
170 'label' => esc_html__('Protection Type', 'king-addons'),
171 'type' => Controls_Manager::SELECT,
172 'options' => [
173 'role' => esc_html__('By Role / Login', 'king-addons'),
174 'password' => $this->get_pro_label(__('By Password', 'king-addons')),
175 'conditions' => esc_html__('By Conditions', 'king-addons'),
176 'locker' => $this->get_pro_label(__('Content Locker', 'king-addons')),
177 ],
178 'default' => 'role',
179 'condition' => [
180 'protected_content_enable' => 'yes',
181 ],
182 ]
183 );
184
185 // =============================================
186 // FALLBACK SETTINGS
187 // =============================================
188
189 $element->add_control(
190 'protected_content_fallback_heading',
191 [
192 'label' => esc_html__('Fallback Options', 'king-addons'),
193 'type' => Controls_Manager::HEADING,
194 'separator' => 'before',
195 'condition' => [
196 'protected_content_enable' => 'yes',
197 ],
198 ]
199 );
200
201 $element->add_control(
202 'protected_content_fallback_type',
203 [
204 'label' => esc_html__('If access denied', 'king-addons'),
205 'type' => Controls_Manager::SELECT,
206 'options' => [
207 'none' => esc_html__('Hide element', 'king-addons'),
208 'message' => esc_html__('Show message', 'king-addons'),
209 'template' => $this->get_pro_label(__('Show template', 'king-addons')),
210 'form' => $this->get_pro_label(__('Show form', 'king-addons')),
211 ],
212 'default' => 'none',
213 'condition' => [
214 'protected_content_enable' => 'yes',
215 ],
216 ]
217 );
218
219 $element->add_control(
220 'protected_content_fallback_msg',
221 [
222 'label' => esc_html__('Message', 'king-addons'),
223 'type' => Controls_Manager::TEXTAREA,
224 'rows' => 3,
225 'default' => esc_html__('This content is protected.', 'king-addons'),
226 'condition' => [
227 'protected_content_enable' => 'yes',
228 'protected_content_fallback_type' => 'message',
229 ],
230 ]
231 );
232
233 $element->add_control(
234 'protected_content_fallback_template',
235 [
236 'label' => esc_html__('Template', 'king-addons'),
237 'type' => Controls_Manager::SELECT2,
238 'options' => $this->get_elementor_templates(),
239 'label_block' => true,
240 'condition' => [
241 'protected_content_enable' => 'yes',
242 'protected_content_fallback_type' => 'template',
243 ],
244 ]
245 );
246
247 $element->add_control(
248 'protected_content_fallback_form',
249 [
250 'label' => esc_html__('Form Shortcode', 'king-addons'),
251 'type' => Controls_Manager::TEXT,
252 'placeholder' => '[contact-form-7 id="123"]',
253 'description' => esc_html__('Enter a form shortcode (CF7, WPForms, etc.)', 'king-addons'),
254 'condition' => [
255 'protected_content_enable' => 'yes',
256 'protected_content_fallback_type' => 'form',
257 ],
258 ]
259 );
260
261 $element->add_control(
262 'protected_content_invert',
263 [
264 'label' => esc_html__('Invert Logic', 'king-addons'),
265 'description' => esc_html__('Show content only when condition is NOT met', 'king-addons'),
266 'type' => Controls_Manager::SWITCHER,
267 'return_value' => 'yes',
268 'default' => '',
269 'condition' => [
270 'protected_content_enable' => 'yes',
271 ],
272 ]
273 );
274
275 // =============================================
276 // ROLE MODE (Free)
277 // =============================================
278
279 $element->add_control(
280 'protected_content_role_heading',
281 [
282 'label' => esc_html__('Role / Login Settings', 'king-addons'),
283 'type' => Controls_Manager::HEADING,
284 'separator' => 'before',
285 'condition' => [
286 'protected_content_enable' => 'yes',
287 'protected_content_mode' => 'role',
288 ],
289 ]
290 );
291
292 $element->add_control(
293 'protected_content_require_login',
294 [
295 'label' => esc_html__('Require Logged In User', 'king-addons'),
296 'type' => Controls_Manager::SWITCHER,
297 'default' => 'yes',
298 'condition' => [
299 'protected_content_enable' => 'yes',
300 'protected_content_mode' => 'role',
301 ],
302 ]
303 );
304
305 $element->add_control(
306 'protected_content_roles',
307 [
308 'label' => esc_html__('Allowed Roles', 'king-addons'),
309 'description' => esc_html__('Leave empty to allow all logged-in users', 'king-addons'),
310 'type' => Controls_Manager::SELECT2,
311 'multiple' => true,
312 'options' => $this->get_wp_roles_for_control(),
313 'condition' => [
314 'protected_content_enable' => 'yes',
315 'protected_content_mode' => 'role',
316 ],
317 ]
318 );
319
320 // =============================================
321 // PASSWORD MODE (Pro)
322 // =============================================
323
324 $element->add_control(
325 'protected_content_password_heading',
326 [
327 'label' => $this->get_pro_label(__('Password Settings', 'king-addons')),
328 'type' => Controls_Manager::HEADING,
329 'separator' => 'before',
330 'condition' => [
331 'protected_content_enable' => 'yes',
332 'protected_content_mode' => 'password',
333 ],
334 ]
335 );
336
337 $element->add_control(
338 'protected_content_password_type',
339 [
340 'label' => esc_html__('Password Scope', 'king-addons'),
341 'type' => Controls_Manager::SELECT,
342 'options' => [
343 'element' => esc_html__('Per Element', 'king-addons'),
344 'global' => esc_html__('Global Key', 'king-addons'),
345 ],
346 'default' => 'element',
347 'condition' => [
348 'protected_content_enable' => 'yes',
349 'protected_content_mode' => 'password',
350 ],
351 ]
352 );
353
354 $element->add_control(
355 'protected_content_password',
356 [
357 'label' => esc_html__('Password', 'king-addons'),
358 'type' => Controls_Manager::TEXT,
359 'input_type' => 'password',
360 'condition' => [
361 'protected_content_enable' => 'yes',
362 'protected_content_mode' => 'password',
363 'protected_content_password_type' => 'element',
364 ],
365 ]
366 );
367
368 $element->add_control(
369 'protected_content_password_key',
370 [
371 'label' => esc_html__('Global Key', 'king-addons'),
372 'description' => esc_html__('Shared key for multiple elements', 'king-addons'),
373 'type' => Controls_Manager::TEXT,
374 'condition' => [
375 'protected_content_enable' => 'yes',
376 'protected_content_mode' => 'password',
377 'protected_content_password_type' => 'global',
378 ],
379 ]
380 );
381
382 $element->add_control(
383 'protected_content_password_global_password',
384 [
385 'label' => esc_html__('Global Password', 'king-addons'),
386 'type' => Controls_Manager::TEXT,
387 'input_type' => 'password',
388 'condition' => [
389 'protected_content_enable' => 'yes',
390 'protected_content_mode' => 'password',
391 'protected_content_password_type' => 'global',
392 ],
393 ]
394 );
395
396 $element->add_control(
397 'protected_content_password_cookie_days',
398 [
399 'label' => esc_html__('Remember for (days)', 'king-addons'),
400 'type' => Controls_Manager::NUMBER,
401 'default' => 7,
402 'min' => 1,
403 'max' => 365,
404 'condition' => [
405 'protected_content_enable' => 'yes',
406 'protected_content_mode' => 'password',
407 ],
408 ]
409 );
410
411 // =============================================
412 // CONDITIONS MODE
413 // =============================================
414
415 $element->add_control(
416 'protected_content_conditions_heading',
417 [
418 'label' => esc_html__('Conditions Settings', 'king-addons'),
419 'type' => Controls_Manager::HEADING,
420 'separator' => 'before',
421 'condition' => [
422 'protected_content_enable' => 'yes',
423 'protected_content_mode' => 'conditions',
424 ],
425 ]
426 );
427
428 // Login Status (Free)
429 $element->add_control(
430 'protected_content_cond_login',
431 [
432 'label' => esc_html__('Login Status', 'king-addons'),
433 'type' => Controls_Manager::SELECT,
434 'options' => [
435 'any' => esc_html__('Any', 'king-addons'),
436 'logged_in' => esc_html__('Logged In', 'king-addons'),
437 'logged_out' => esc_html__('Logged Out', 'king-addons'),
438 ],
439 'default' => 'any',
440 'condition' => [
441 'protected_content_enable' => 'yes',
442 'protected_content_mode' => 'conditions',
443 ],
444 ]
445 );
446
447 // Device (Free)
448 $element->add_control(
449 'protected_content_cond_device',
450 [
451 'label' => esc_html__('Allowed Devices', 'king-addons'),
452 'description' => esc_html__('Leave empty to allow all devices', 'king-addons'),
453 'type' => Controls_Manager::SELECT2,
454 'multiple' => true,
455 'options' => [
456 'desktop' => esc_html__('Desktop', 'king-addons'),
457 'tablet' => esc_html__('Tablet', 'king-addons'),
458 'mobile' => esc_html__('Mobile', 'king-addons'),
459 ],
460 'condition' => [
461 'protected_content_enable' => 'yes',
462 'protected_content_mode' => 'conditions',
463 ],
464 ]
465 );
466
467 // Browser (Pro)
468 $element->add_control(
469 'protected_content_cond_browser',
470 [
471 'label' => $this->get_pro_label(__('Browser', 'king-addons')),
472 'description' => esc_html__('Leave empty to allow all browsers', 'king-addons'),
473 'type' => Controls_Manager::SELECT2,
474 'multiple' => true,
475 'options' => [
476 'chrome' => esc_html__('Chrome', 'king-addons'),
477 'firefox' => esc_html__('Firefox', 'king-addons'),
478 'safari' => esc_html__('Safari', 'king-addons'),
479 'edge' => esc_html__('Edge', 'king-addons'),
480 'opera' => esc_html__('Opera', 'king-addons'),
481 'ie' => esc_html__('Internet Explorer', 'king-addons'),
482 ],
483 'condition' => [
484 'protected_content_enable' => 'yes',
485 'protected_content_mode' => 'conditions',
486 ],
487 ]
488 );
489
490 // Date Range (Pro)
491 $element->add_control(
492 'protected_content_date_heading',
493 [
494 'label' => $this->get_pro_label(__('Date Range', 'king-addons')),
495 'type' => Controls_Manager::HEADING,
496 'separator' => 'before',
497 'condition' => [
498 'protected_content_enable' => 'yes',
499 'protected_content_mode' => 'conditions',
500 ],
501 ]
502 );
503
504 $element->add_control(
505 'protected_content_cond_date_from',
506 [
507 'label' => esc_html__('From Date', 'king-addons'),
508 'type' => Controls_Manager::DATE_TIME,
509 'condition' => [
510 'protected_content_enable' => 'yes',
511 'protected_content_mode' => 'conditions',
512 ],
513 ]
514 );
515
516 $element->add_control(
517 'protected_content_cond_date_to',
518 [
519 'label' => esc_html__('To Date', 'king-addons'),
520 'type' => Controls_Manager::DATE_TIME,
521 'condition' => [
522 'protected_content_enable' => 'yes',
523 'protected_content_mode' => 'conditions',
524 ],
525 ]
526 );
527
528 // URL Parameters (Pro)
529 $element->add_control(
530 'protected_content_url_heading',
531 [
532 'label' => $this->get_pro_label(__('URL Parameters', 'king-addons')),
533 'type' => Controls_Manager::HEADING,
534 'separator' => 'before',
535 'condition' => [
536 'protected_content_enable' => 'yes',
537 'protected_content_mode' => 'conditions',
538 ],
539 ]
540 );
541
542 $element->add_control(
543 'protected_content_cond_url_param',
544 [
545 'label' => esc_html__('URL Parameter Name', 'king-addons'),
546 'type' => Controls_Manager::TEXT,
547 'placeholder' => 'ref',
548 'description' => esc_html__('e.g., "ref" for ?ref=value', 'king-addons'),
549 'condition' => [
550 'protected_content_enable' => 'yes',
551 'protected_content_mode' => 'conditions',
552 ],
553 ]
554 );
555
556 $element->add_control(
557 'protected_content_cond_url_value',
558 [
559 'label' => esc_html__('URL Parameter Value', 'king-addons'),
560 'type' => Controls_Manager::TEXT,
561 'placeholder' => 'campaign',
562 'description' => esc_html__('Leave empty to check only parameter existence', 'king-addons'),
563 'condition' => [
564 'protected_content_enable' => 'yes',
565 'protected_content_mode' => 'conditions',
566 ],
567 ]
568 );
569
570 // WooCommerce Conditions (Pro)
571 $element->add_control(
572 'protected_content_woo_heading',
573 [
574 'label' => $this->get_pro_label(__('WooCommerce Conditions', 'king-addons')),
575 'type' => Controls_Manager::HEADING,
576 'separator' => 'before',
577 'condition' => [
578 'protected_content_enable' => 'yes',
579 'protected_content_mode' => 'conditions',
580 ],
581 ]
582 );
583
584 $element->add_control(
585 'protected_content_cond_woo_in_cart_product_ids',
586 [
587 'label' => esc_html__('Products in Cart', 'king-addons'),
588 'type' => Controls_Manager::TEXT,
589 'placeholder' => '123, 456, 789',
590 'description' => esc_html__('Comma-separated product IDs. Show if ANY is in cart.', 'king-addons'),
591 'condition' => [
592 'protected_content_enable' => 'yes',
593 'protected_content_mode' => 'conditions',
594 ],
595 ]
596 );
597
598 $element->add_control(
599 'protected_content_cond_woo_min_cart_total',
600 [
601 'label' => esc_html__('Minimum Cart Total', 'king-addons'),
602 'type' => Controls_Manager::NUMBER,
603 'placeholder' => '50.00',
604 'description' => esc_html__('Show content if cart total is at least this amount', 'king-addons'),
605 'condition' => [
606 'protected_content_enable' => 'yes',
607 'protected_content_mode' => 'conditions',
608 ],
609 ]
610 );
611
612 $element->add_control(
613 'protected_content_cond_woo_customer_bought_ids',
614 [
615 'label' => esc_html__('Customer Bought Products', 'king-addons'),
616 'type' => Controls_Manager::TEXT,
617 'placeholder' => '123, 456',
618 'description' => esc_html__('Comma-separated product IDs. Show if user purchased ALL.', 'king-addons'),
619 'condition' => [
620 'protected_content_enable' => 'yes',
621 'protected_content_mode' => 'conditions',
622 ],
623 ]
624 );
625
626 // =============================================
627 // LOCKER MODE (Pro)
628 // =============================================
629
630 $element->add_control(
631 'protected_content_locker_heading',
632 [
633 'label' => $this->get_pro_label(__('Content Locker Settings', 'king-addons')),
634 'type' => Controls_Manager::HEADING,
635 'separator' => 'before',
636 'condition' => [
637 'protected_content_enable' => 'yes',
638 'protected_content_mode' => 'locker',
639 ],
640 ]
641 );
642
643 $element->add_control(
644 'protected_content_locker_method',
645 [
646 'label' => esc_html__('Unlock Method', 'king-addons'),
647 'type' => Controls_Manager::SELECT,
648 'options' => [
649 'form_submit' => esc_html__('After Form Submit', 'king-addons'),
650 'manual_token' => esc_html__('By Token', 'king-addons'),
651 ],
652 'default' => 'form_submit',
653 'condition' => [
654 'protected_content_enable' => 'yes',
655 'protected_content_mode' => 'locker',
656 ],
657 ]
658 );
659
660 $element->add_control(
661 'protected_content_locker_token',
662 [
663 'label' => esc_html__('Locker Token', 'king-addons'),
664 'description' => esc_html__('Unique identifier for this locked content', 'king-addons'),
665 'type' => Controls_Manager::TEXT,
666 'placeholder' => 'newsletter-unlock',
667 'condition' => [
668 'protected_content_enable' => 'yes',
669 'protected_content_mode' => 'locker',
670 ],
671 ]
672 );
673
674 $element->add_control(
675 'protected_content_locker_form',
676 [
677 'label' => esc_html__('Form Shortcode', 'king-addons'),
678 'description' => esc_html__('Form to display for unlocking content', 'king-addons'),
679 'type' => Controls_Manager::TEXT,
680 'placeholder' => '[contact-form-7 id="123"]',
681 'condition' => [
682 'protected_content_enable' => 'yes',
683 'protected_content_mode' => 'locker',
684 'protected_content_locker_method' => 'form_submit',
685 ],
686 ]
687 );
688
689 $element->add_control(
690 'protected_content_locker_cookie_days',
691 [
692 'label' => esc_html__('Remember for (days)', 'king-addons'),
693 'type' => Controls_Manager::NUMBER,
694 'default' => 7,
695 'min' => 1,
696 'max' => 365,
697 'condition' => [
698 'protected_content_enable' => 'yes',
699 'protected_content_mode' => 'locker',
700 ],
701 ]
702 );
703
704 $element->end_controls_section();
705 }
706
707 /**
708 * Before render hook.
709 *
710 * @param Element_Base $element Element.
711 *
712 * @return void
713 */
714 public function before_render_element(Element_Base $element): void
715 {
716 $settings = $element->get_settings_for_display();
717
718 if (empty($settings['protected_content_enable']) || 'yes' !== $settings['protected_content_enable']) {
719 return;
720 }
721
722 // In editor or preview: show content, mark protected.
723 if ($this->is_editor_or_preview()) {
724 $element->add_render_attribute('_wrapper', 'data-king-protected', 'yes');
725 $element->add_render_attribute('_wrapper', 'class', 'king-addons-protected-preview');
726
727 // Add protection type info for editor badge.
728 $protection_info = $this->get_protection_info($settings);
729 $element->add_render_attribute('_wrapper', 'data-king-protected-type', esc_attr($protection_info));
730 return;
731 }
732
733 $has_access = $this->evaluate_access($settings, $element);
734
735 if (!empty($settings['protected_content_invert']) && 'yes' === $settings['protected_content_invert']) {
736 $has_access = !$has_access;
737 }
738
739 if ($has_access) {
740 return;
741 }
742
743 // Special handling for password mode - show password form.
744 $mode = $settings['protected_content_mode'] ?? 'role';
745 if ('password' === $mode && $this->can_use_pro()) {
746 $this->render_password_form($settings, $element);
747 $this->suppress_element_render($element);
748 return;
749 }
750
751 $fallback = $settings['protected_content_fallback_type'] ?? 'none';
752
753 switch ($fallback) {
754 case 'message':
755 $this->render_fallback_message($settings);
756 $this->suppress_element_render($element);
757 return;
758
759 case 'template':
760 if ($this->can_use_pro() && !empty($settings['protected_content_fallback_template'])) {
761 $this->render_fallback_template((int) $settings['protected_content_fallback_template']);
762 $this->suppress_element_render($element);
763 return;
764 }
765 $this->render_fallback_message($settings);
766 $this->suppress_element_render($element);
767 return;
768
769 case 'form':
770 if ($this->can_use_pro() && !empty($settings['protected_content_fallback_form'])) {
771 echo '<div class="king-addons-protected-fallback king-addons-protected-form-fallback">';
772 echo do_shortcode($settings['protected_content_fallback_form']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
773 echo '</div>';
774 $this->suppress_element_render($element);
775 return;
776 }
777 $this->render_fallback_message($settings);
778 $this->suppress_element_render($element);
779 return;
780
781 case 'none':
782 default:
783 $this->suppress_element_render($element);
784 return;
785 }
786 }
787
788 /**
789 * Evaluate access.
790 *
791 * @param array<string,mixed> $settings Settings.
792 * @param Element_Base $element Element.
793 *
794 * @return bool
795 */
796 private function evaluate_access(array $settings, Element_Base $element): bool
797 {
798 $mode = $settings['protected_content_mode'] ?? 'role';
799
800 switch ($mode) {
801 case 'role':
802 return $this->check_role_access($settings);
803
804 case 'password':
805 return $this->check_password_access($settings, $element);
806
807 case 'conditions':
808 return $this->check_conditions_access($settings);
809
810 case 'locker':
811 return $this->check_locker_access($settings);
812
813 default:
814 return true;
815 }
816 }
817
818 /**
819 * Role-based access.
820 *
821 * @param array<string,mixed> $settings Settings.
822 *
823 * @return bool
824 */
825 private function check_role_access(array $settings): bool
826 {
827 $require_login = !empty($settings['protected_content_require_login']) && 'yes' === $settings['protected_content_require_login'];
828
829 if (!is_user_logged_in()) {
830 return !$require_login;
831 }
832
833 // User is logged in.
834 if (empty($settings['protected_content_roles']) || !is_array($settings['protected_content_roles'])) {
835 // No specific roles required - all logged in users have access.
836 return true;
837 }
838
839 $user = wp_get_current_user();
840 foreach ($user->roles as $role) {
841 if (in_array($role, $settings['protected_content_roles'], true)) {
842 return true;
843 }
844 }
845
846 return false;
847 }
848
849 /**
850 * Password access.
851 *
852 * @param array<string,mixed> $settings Settings.
853 * @param Element_Base $element Element.
854 *
855 * @return bool
856 */
857 private function check_password_access(array $settings, Element_Base $element): bool
858 {
859 if (!$this->can_use_pro()) {
860 return true;
861 }
862
863 $scope = $settings['protected_content_password_type'] ?? 'element';
864
865 if ('element' === $scope) {
866 $cookie_name = 'king_protect_' . $element->get_id();
867 return !empty($_COOKIE[$cookie_name]) && '1' === $_COOKIE[$cookie_name];
868 }
869
870 // Global scope.
871 $key = $settings['protected_content_password_key'] ?? '';
872 if (empty($key)) {
873 return false;
874 }
875
876 $cookie_name = 'king_protect_global_' . sanitize_key($key);
877 return !empty($_COOKIE[$cookie_name]) && '1' === $_COOKIE[$cookie_name];
878 }
879
880 /**
881 * Conditions access.
882 *
883 * @param array<string,mixed> $settings Settings.
884 *
885 * @return bool
886 */
887 private function check_conditions_access(array $settings): bool
888 {
889 // Login condition (Free).
890 $login_cond = $settings['protected_content_cond_login'] ?? 'any';
891 if ('logged_in' === $login_cond && !is_user_logged_in()) {
892 return false;
893 }
894 if ('logged_out' === $login_cond && is_user_logged_in()) {
895 return false;
896 }
897
898 // Device condition (Free).
899 if (!empty($settings['protected_content_cond_device']) && is_array($settings['protected_content_cond_device'])) {
900 $device = $this->detect_device();
901 if (!in_array($device, $settings['protected_content_cond_device'], true)) {
902 return false;
903 }
904 }
905
906 // Pro conditions.
907 if ($this->can_use_pro()) {
908 // Browser condition.
909 if (!empty($settings['protected_content_cond_browser']) && is_array($settings['protected_content_cond_browser'])) {
910 $browser = $this->detect_browser();
911 if (!in_array($browser, $settings['protected_content_cond_browser'], true)) {
912 return false;
913 }
914 }
915
916 // Date range.
917 $now = wp_date('U'); // Use WordPress timezone.
918 if (!empty($settings['protected_content_cond_date_from'])) {
919 $from = strtotime((string) $settings['protected_content_cond_date_from']);
920 if ($from && $now < $from) {
921 return false;
922 }
923 }
924 if (!empty($settings['protected_content_cond_date_to'])) {
925 $to = strtotime((string) $settings['protected_content_cond_date_to']);
926 if ($to && $now > $to) {
927 return false;
928 }
929 }
930
931 // URL parameter.
932 if (!empty($settings['protected_content_cond_url_param'])) {
933 $param = sanitize_text_field($settings['protected_content_cond_url_param']);
934 $value = $settings['protected_content_cond_url_value'] ?? '';
935
936 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
937 if (!isset($_GET[$param])) {
938 return false;
939 }
940
941 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
942 if ('' !== $value && sanitize_text_field($_GET[$param]) !== $value) {
943 return false;
944 }
945 }
946
947 // WooCommerce conditions.
948 if (class_exists('WooCommerce')) {
949 // Products in cart.
950 if (!empty($settings['protected_content_cond_woo_in_cart_product_ids'])) {
951 $ids = $this->sanitize_ids($settings['protected_content_cond_woo_in_cart_product_ids']);
952 if (!empty($ids) && !$this->woo_cart_has_products($ids)) {
953 return false;
954 }
955 }
956
957 // Minimum cart total.
958 if (!empty($settings['protected_content_cond_woo_min_cart_total'])) {
959 $min_total = (float) $settings['protected_content_cond_woo_min_cart_total'];
960 $total = (float) (WC()->cart ? WC()->cart->get_total('edit') : 0);
961 if ($total < $min_total) {
962 return false;
963 }
964 }
965
966 // Customer bought products.
967 if (!empty($settings['protected_content_cond_woo_customer_bought_ids']) && is_user_logged_in()) {
968 $ids = $this->sanitize_ids($settings['protected_content_cond_woo_customer_bought_ids']);
969 if (!empty($ids)) {
970 $user = wp_get_current_user();
971 foreach ($ids as $pid) {
972 if (!wc_customer_bought_product($user->user_email, $user->ID, $pid)) {
973 return false;
974 }
975 }
976 }
977 }
978 }
979 }
980
981 return true;
982 }
983
984 /**
985 * Locker access.
986 *
987 * @param array<string,mixed> $settings Settings.
988 *
989 * @return bool
990 */
991 private function check_locker_access(array $settings): bool
992 {
993 if (!$this->can_use_pro()) {
994 return true;
995 }
996
997 $token = $settings['protected_content_locker_token'] ?? '';
998 if (empty($token)) {
999 return false;
1000 }
1001
1002 $cookie_name = 'king_locker_' . sanitize_key($token);
1003 return !empty($_COOKIE[$cookie_name]) && '1' === $_COOKIE[$cookie_name];
1004 }
1005
1006 /**
1007 * Set locker cookie after unlock event.
1008 *
1009 * @param string $token Token.
1010 * @param int $days Days to remember (default 7).
1011 *
1012 * @return void
1013 */
1014 public function set_locker_cookie(string $token, int $days = 7): void
1015 {
1016 $cookie_name = 'king_locker_' . sanitize_key($token);
1017 $expiry = time() + ($days * DAY_IN_SECONDS);
1018 setcookie($cookie_name, '1', $expiry, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
1019 }
1020
1021 /**
1022 * AJAX handler for password verification.
1023 *
1024 * @return void
1025 */
1026 public function ajax_verify_password(): void
1027 {
1028 check_ajax_referer('king_addons_protected_content', 'nonce');
1029
1030 if (!$this->can_use_pro()) {
1031 wp_send_json_error(['message' => esc_html__('Pro feature', 'king-addons')]);
1032 }
1033
1034 $password = isset($_POST['password']) ? sanitize_text_field($_POST['password']) : '';
1035 $element_id = isset($_POST['element_id']) ? sanitize_text_field($_POST['element_id']) : '';
1036 $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
1037 $scope = isset($_POST['scope']) ? sanitize_text_field($_POST['scope']) : 'element';
1038 $global_key = isset($_POST['global_key']) ? sanitize_text_field($_POST['global_key']) : '';
1039 $days = isset($_POST['days']) ? absint($_POST['days']) : 7;
1040
1041 if (empty($password) || empty($post_id)) {
1042 wp_send_json_error(['message' => esc_html__('Invalid request', 'king-addons')]);
1043 }
1044
1045 // Get element settings from post meta.
1046 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
1047 if (empty($elementor_data)) {
1048 wp_send_json_error(['message' => esc_html__('Element not found', 'king-addons')]);
1049 }
1050
1051 $data = json_decode($elementor_data, true);
1052 if (!is_array($data)) {
1053 wp_send_json_error(['message' => esc_html__('Invalid data', 'king-addons')]);
1054 }
1055
1056 // Find element and get stored password.
1057 $stored_password = $this->find_element_password($data, $element_id, $scope, $global_key);
1058
1059 if (null === $stored_password) {
1060 wp_send_json_error(['message' => esc_html__('Configuration error', 'king-addons')]);
1061 }
1062
1063 if ($password !== $stored_password) {
1064 wp_send_json_error(['message' => esc_html__('Incorrect password', 'king-addons')]);
1065 }
1066
1067 // Set cookie.
1068 $expiry = time() + ($days * DAY_IN_SECONDS);
1069 if ('global' === $scope && !empty($global_key)) {
1070 $cookie_name = 'king_protect_global_' . sanitize_key($global_key);
1071 } else {
1072 $cookie_name = 'king_protect_' . $element_id;
1073 }
1074
1075 setcookie($cookie_name, '1', $expiry, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
1076
1077 wp_send_json_success(['message' => esc_html__('Access granted', 'king-addons')]);
1078 }
1079
1080 /**
1081 * Find element password in Elementor data.
1082 *
1083 * @param array<mixed> $data Elementor data.
1084 * @param string $element_id Element ID.
1085 * @param string $scope Scope (element/global).
1086 * @param string $global_key Global key.
1087 *
1088 * @return string|null
1089 */
1090 private function find_element_password(array $data, string $element_id, string $scope, string $global_key): ?string
1091 {
1092 foreach ($data as $element) {
1093 if (!is_array($element)) {
1094 continue;
1095 }
1096
1097 $id = $element['id'] ?? '';
1098 $settings = $element['settings'] ?? [];
1099
1100 if ($id === $element_id) {
1101 if ('global' === $scope) {
1102 return $settings['protected_content_password_global_password'] ?? null;
1103 }
1104 return $settings['protected_content_password'] ?? null;
1105 }
1106
1107 // Recursively search in children.
1108 if (!empty($element['elements']) && is_array($element['elements'])) {
1109 $found = $this->find_element_password($element['elements'], $element_id, $scope, $global_key);
1110 if (null !== $found) {
1111 return $found;
1112 }
1113 }
1114 }
1115
1116 return null;
1117 }
1118
1119 /**
1120 * Render password form.
1121 *
1122 * @param array<string,mixed> $settings Settings.
1123 * @param Element_Base $element Element.
1124 *
1125 * @return void
1126 */
1127 private function render_password_form(array $settings, Element_Base $element): void
1128 {
1129 $scope = $settings['protected_content_password_type'] ?? 'element';
1130 $global_key = $settings['protected_content_password_key'] ?? '';
1131 $days = (int) ($settings['protected_content_password_cookie_days'] ?? 7);
1132 $message = $settings['protected_content_fallback_msg'] ?? esc_html__('This content is password protected.', 'king-addons');
1133
1134 ?>
1135 <div class="king-addons-protected-fallback king-addons-protected-password-form">
1136 <div class="king-addons-protected-password-message"><?php echo esc_html($message); ?></div>
1137 <form class="king-addons-password-form"
1138 data-element-id="<?php echo esc_attr($element->get_id()); ?>"
1139 data-post-id="<?php echo esc_attr(get_the_ID()); ?>"
1140 data-scope="<?php echo esc_attr($scope); ?>"
1141 data-global-key="<?php echo esc_attr($global_key); ?>"
1142 data-days="<?php echo esc_attr($days); ?>">
1143 <div class="king-addons-password-input-wrap">
1144 <input type="password"
1145 class="king-addons-password-input"
1146 placeholder="<?php esc_attr_e('Enter password', 'king-addons'); ?>"
1147 required />
1148 <button type="submit" class="king-addons-password-submit">
1149 <span class="king-addons-password-submit-text"><?php esc_html_e('Unlock', 'king-addons'); ?></span>
1150 <span class="king-addons-password-submit-loading" style="display:none;"></span>
1151 </button>
1152 </div>
1153 <div class="king-addons-password-error" style="display:none;"></div>
1154 </form>
1155 </div>
1156 <?php
1157 }
1158
1159 /**
1160 * Render fallback message.
1161 *
1162 * @param array<string,mixed> $settings Settings.
1163 *
1164 * @return void
1165 */
1166 private function render_fallback_message(array $settings): void
1167 {
1168 $msg = $settings['protected_content_fallback_msg'] ?? esc_html__('This content is protected.', 'king-addons');
1169 echo '<div class="king-addons-protected-fallback">' . esc_html($msg) . '</div>';
1170 }
1171
1172 /**
1173 * Render Elementor template.
1174 *
1175 * @param int $template_id Template ID.
1176 *
1177 * @return void
1178 */
1179 private function render_fallback_template(int $template_id): void
1180 {
1181 $frontend = Plugin::$instance->frontend;
1182 echo '<div class="king-addons-protected-fallback king-addons-protected-template-fallback">';
1183 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1184 echo $frontend->get_builder_content_for_display($template_id);
1185 echo '</div>';
1186 }
1187
1188 /**
1189 * Suppress element rendering.
1190 *
1191 * @param Element_Base $element Element.
1192 *
1193 * @return void
1194 */
1195 private function suppress_element_render(Element_Base $element): void
1196 {
1197 // Try modern Elementor method first.
1198 if (method_exists($element, 'set_should_render')) {
1199 $element->set_should_render(false);
1200 return;
1201 }
1202
1203 // Fallback: hide with CSS.
1204 $element->add_render_attribute('_wrapper', 'class', 'king-addons-protected-hidden');
1205 }
1206
1207 /**
1208 * Check if in editor or preview mode.
1209 *
1210 * @return bool
1211 */
1212 private function is_editor_or_preview(): bool
1213 {
1214 if (!class_exists('\Elementor\Plugin')) {
1215 return false;
1216 }
1217
1218 $editor = Plugin::$instance->editor;
1219 $preview = Plugin::$instance->preview;
1220
1221 if ($editor && $editor->is_edit_mode()) {
1222 return true;
1223 }
1224
1225 if ($preview && $preview->is_preview_mode()) {
1226 return true;
1227 }
1228
1229 return false;
1230 }
1231
1232 /**
1233 * Get protection info for editor badge.
1234 *
1235 * @param array<string,mixed> $settings Settings.
1236 *
1237 * @return string
1238 */
1239 private function get_protection_info(array $settings): string
1240 {
1241 $mode = $settings['protected_content_mode'] ?? 'role';
1242 $info = [];
1243
1244 switch ($mode) {
1245 case 'role':
1246 $info[] = 'Role/Login';
1247 if (!empty($settings['protected_content_roles'])) {
1248 $info[] = implode(', ', (array) $settings['protected_content_roles']);
1249 }
1250 break;
1251
1252 case 'password':
1253 $scope = $settings['protected_content_password_type'] ?? 'element';
1254 $info[] = 'Password (' . $scope . ')';
1255 break;
1256
1257 case 'conditions':
1258 $info[] = 'Conditions';
1259 if (!empty($settings['protected_content_cond_login']) && 'any' !== $settings['protected_content_cond_login']) {
1260 $info[] = $settings['protected_content_cond_login'];
1261 }
1262 if (!empty($settings['protected_content_cond_device'])) {
1263 $info[] = 'Device: ' . implode(', ', (array) $settings['protected_content_cond_device']);
1264 }
1265 break;
1266
1267 case 'locker':
1268 $info[] = 'Content Locker';
1269 if (!empty($settings['protected_content_locker_token'])) {
1270 $info[] = 'Token: ' . $settings['protected_content_locker_token'];
1271 }
1272 break;
1273 }
1274
1275 return implode(' | ', $info);
1276 }
1277
1278 /**
1279 * Detect device type.
1280 *
1281 * @return string desktop|tablet|mobile
1282 */
1283 private function detect_device(): string
1284 {
1285 if (!isset($_SERVER['HTTP_USER_AGENT'])) {
1286 return 'desktop';
1287 }
1288
1289 $ua = strtolower($_SERVER['HTTP_USER_AGENT']);
1290
1291 // Tablet detection (before mobile since tablets often have "mobile" in UA).
1292 $tablets = ['ipad', 'tablet', 'kindle', 'silk', 'playbook'];
1293 foreach ($tablets as $tablet) {
1294 if (strpos($ua, $tablet) !== false) {
1295 return 'tablet';
1296 }
1297 }
1298
1299 // Android tablet (no "mobile" in UA).
1300 if (strpos($ua, 'android') !== false && strpos($ua, 'mobile') === false) {
1301 return 'tablet';
1302 }
1303
1304 // Mobile detection.
1305 if (wp_is_mobile()) {
1306 return 'mobile';
1307 }
1308
1309 return 'desktop';
1310 }
1311
1312 /**
1313 * Detect browser.
1314 *
1315 * @return string
1316 */
1317 private function detect_browser(): string
1318 {
1319 if (!isset($_SERVER['HTTP_USER_AGENT'])) {
1320 return 'unknown';
1321 }
1322
1323 $ua = $_SERVER['HTTP_USER_AGENT'];
1324
1325 // Order matters: Edge contains "Chrome", Chrome contains "Safari".
1326 if (preg_match('/Edg/i', $ua)) {
1327 return 'edge';
1328 }
1329 if (preg_match('/OPR|Opera/i', $ua)) {
1330 return 'opera';
1331 }
1332 if (preg_match('/Chrome/i', $ua)) {
1333 return 'chrome';
1334 }
1335 if (preg_match('/Safari/i', $ua)) {
1336 return 'safari';
1337 }
1338 if (preg_match('/Firefox/i', $ua)) {
1339 return 'firefox';
1340 }
1341 if (preg_match('/MSIE|Trident/i', $ua)) {
1342 return 'ie';
1343 }
1344
1345 return 'unknown';
1346 }
1347
1348 /**
1349 * Get WP roles as options with caching.
1350 *
1351 * @return array<string,string>
1352 */
1353 private function get_wp_roles_for_control(): array
1354 {
1355 if (null !== self::$roles_cache) {
1356 return self::$roles_cache;
1357 }
1358
1359 global $wp_roles;
1360
1361 if (!isset($wp_roles)) {
1362 $wp_roles = wp_roles();
1363 }
1364
1365 self::$roles_cache = [];
1366 foreach ($wp_roles->roles as $key => $role) {
1367 self::$roles_cache[$key] = $role['name'];
1368 }
1369
1370 return self::$roles_cache;
1371 }
1372
1373 /**
1374 * Get Elementor templates with caching.
1375 *
1376 * @return array<int,string>
1377 */
1378 private function get_elementor_templates(): array
1379 {
1380 if (null !== self::$templates_cache) {
1381 return self::$templates_cache;
1382 }
1383
1384 self::$templates_cache = [];
1385
1386 $templates = get_posts([
1387 'post_type' => 'elementor_library',
1388 'post_status' => 'publish',
1389 'posts_per_page' => -1,
1390 'orderby' => 'title',
1391 'order' => 'ASC',
1392 ]);
1393
1394 foreach ($templates as $template) {
1395 self::$templates_cache[$template->ID] = $template->post_title;
1396 }
1397
1398 return self::$templates_cache;
1399 }
1400
1401 /**
1402 * Check Woo cart for products.
1403 *
1404 * @param array<int> $ids Product IDs.
1405 *
1406 * @return bool
1407 */
1408 private function woo_cart_has_products(array $ids): bool
1409 {
1410 if (!function_exists('WC') || !WC()->cart) {
1411 return false;
1412 }
1413
1414 foreach (WC()->cart->get_cart() as $cart_item) {
1415 $pid = (int) ($cart_item['product_id'] ?? 0);
1416 $vid = (int) ($cart_item['variation_id'] ?? 0);
1417
1418 if (in_array($pid, $ids, true) || ($vid > 0 && in_array($vid, $ids, true))) {
1419 return true;
1420 }
1421 }
1422
1423 return false;
1424 }
1425
1426 /**
1427 * Sanitize comma-separated IDs.
1428 *
1429 * @param string $raw Raw string.
1430 *
1431 * @return array<int>
1432 */
1433 private function sanitize_ids(string $raw): array
1434 {
1435 $ids = array_filter(array_map('absint', array_map('trim', explode(',', $raw))));
1436 return array_values($ids);
1437 }
1438
1439 /**
1440 * Is pro available.
1441 *
1442 * @return bool
1443 */
1444 private function can_use_pro(): bool
1445 {
1446 if (!function_exists('king_addons_freemius')) {
1447 return false;
1448 }
1449
1450 return king_addons_freemius()->can_use_premium_code__premium_only();
1451 }
1452
1453 /**
1454 * Get label with Pro icon.
1455 *
1456 * @param string $label Label text.
1457 *
1458 * @return string
1459 */
1460 private function get_pro_label(string $label): string
1461 {
1462 if ($this->can_use_pro()) {
1463 return $label;
1464 }
1465
1466 return sprintf('%s <i class="eicon-pro-icon"></i>', $label);
1467 }
1468 }
1469
1470
1471
1472
1473
1474
1475
1476