PluginProbe
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment / trunk
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment vtrunk
3.1.13 3.1.12 3.1.11 3.1.10 3.1.9 3.1.8 3.1.7 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 All 122 releases
firebox / Inc / Core / FB / Box.php

Box.php in FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment trunk, at Inc/Core/FB/Box.php

1,123 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package FireBox
4 * @version 3.1.13 Free
5 *
6 * @author FirePlugins <info@fireplugins.com>
7 * @link https://www.fireplugins.com
8 * @copyright Copyright © 2026 FirePlugins All Rights Reserved
9 * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
10 */
11
12 namespace FireBox\Core\FB;
13
14 if (!defined('ABSPATH'))
15 {
16 exit; // Exit if accessed directly.
17 }
18
19 use FireBox\Core\Helpers\BoxHelper;
20 use FPFramework\Libs\Registry;
21 use FPFramework\Helpers\Fields\DimensionsHelper;
22 use FPFramework\Helpers\CSS;
23
24 class Box
25 {
26 /**
27 * Send useful JS snippet once in first box
28 *
29 * @var boolean
30 */
31 static $loadedLocalizedScript = false;
32
33 /**
34 * The box.
35 *
36 * @param object
37 */
38 private $box = null;
39
40 /**
41 * Factory
42 *
43 * @var Factory
44 */
45 private $factory = null;
46
47 /**
48 * FireBox settings.
49 *
50 * @var object
51 */
52 private $params = null;
53
54 /**
55 * Popup CSS.
56 *
57 * @var CSS
58 */
59 public $css = null;
60
61 /**
62 * Display condition groups that must be resolved in the browser before the
63 * campaign may open (session-dependent rules like Time on Site/Pageviews).
64 * Populated by pass(), shipped to the frontend runtime via prepare().
65 *
66 * @var array
67 */
68 private $clientRuleGroups = [];
69
70 /**
71 * Constructor.
72 *
73 * @param object $box
74 * @param object $factory
75 *
76 * @return void
77 */
78 public function __construct($box = null, $factory = null)
79 {
80 if ($box)
81 {
82 $this->box = $this->prepareConstructorBox($box);
83 }
84
85 if (!$factory)
86 {
87 $factory = new \FPFramework\Base\Factory();
88 }
89 $this->factory = $factory;
90
91 $this->params = new Registry(BoxHelper::getParams());
92 }
93
94 /**
95 * Allow to set either a box ID or box object
96 * and we then set the box object.
97 *
98 * @param mixed $box
99 *
100 * @return object
101 */
102 private function prepareConstructorBox($box)
103 {
104 if (!is_object($box))
105 {
106 $box = $this->get($box);
107 }
108
109 return $box;
110 }
111
112 /**
113 * Get a box.
114 *
115 * @param int $id
116 * @param string $status
117 *
118 * @return object|null
119 */
120 public function get($id = null, $status = null)
121 {
122 if (!$id)
123 {
124 return null;
125 }
126
127 $payload = [
128 'where' => [
129 'ID' => ' = ' . intval($id),
130 'post_type' => " = 'firebox'"
131 ]
132 ];
133
134 // apply status if given
135 if ($status)
136 {
137 $payload['where']['post_status'] = ' = \'' . sanitize_key($status) . '\'';
138 }
139
140 if (!$box = firebox()->tables->box->getResults($payload))
141 {
142 return null;
143 }
144
145 if (!isset($box[0]))
146 {
147 return null;
148 }
149
150 $this->box = $box[0];
151
152 // get meta options for box
153 $meta = \FireBox\Core\Helpers\BoxHelper::getMeta($id);
154 $this->box->params = new Registry($meta);
155
156 return $this->box;
157 }
158
159 /**
160 * Renders the box.
161 *
162 * @return void
163 */
164 public function render()
165 {
166 // Check Publishing Assignments
167 if (!$this->pass())
168 {
169 return false;
170 }
171
172 $fbox = $this->box;
173
174 /**
175 * Runs before rendering the box.
176 */
177 $this->box = apply_filters('firebox/box/before_render', $this->box);
178
179 $this->prepare();
180
181 $css = $this->getCustomCSS();
182
183 add_action('wp_enqueue_scripts', function() use ($fbox, $css) {
184 // Loads all media files.
185 $this->loadBoxMedia($fbox);
186
187 // Load CSS
188 if ($css)
189 {
190 wp_add_inline_style('firebox', $css);
191 }
192
193
194 });
195
196 // Allow to manipulate the box before rendering
197 $this->box = apply_filters('firebox/box/edit', $this->box);
198
199 // payload
200 $payload = [
201 'box' => $this->box,
202 'params' => $this->params,
203 ];
204
205 // print campaign HTML
206 add_action('wp_footer', function() use ($payload) {
207 echo $this->getFinalCampaignHTML($payload); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
208 });
209
210 return true;
211 }
212
213 public function renderEmbed()
214 {
215 // Check Publishing Assignments
216 if (!$this->pass())
217 {
218 return false;
219 }
220
221 /**
222 * Runs before rendering the box.
223 */
224 $this->box = apply_filters('firebox/box/before_render', $this->box);
225
226 $this->prepare();
227
228 // Loads all media files.
229 $this->loadBoxMedia($this->box);
230
231 $css = $this->getCustomCSS();
232
233 wp_register_style('fireboxStyle', false);
234 wp_enqueue_style('fireboxStyle');
235
236 // Load CSS
237 if ($css)
238 {
239 wp_add_inline_style('fireboxStyle', $css);
240 }
241
242
243
244 // Allow to manipulate the box before rendering
245 $this->box = apply_filters('firebox/box/edit', $this->box);
246
247 // payload
248 $payload = [
249 'box' => $this->box,
250 'params' => $this->params,
251 ];
252
253 // return box template
254 return $this->getFinalCampaignHTML($payload);
255 }
256
257 public function getFinalCampaignHTML($payload)
258 {
259 $html = firebox()->renderer->public->render('box', $payload, true);
260
261 /**
262 * Runs after rendering the box.
263 */
264 return apply_filters('firebox/box/after_render', $html, $payload['box']);
265 }
266
267 /**
268 * Gets the Custom CSS of the popup.
269 *
270 * @return string
271 */
272 public function getCustomCSS()
273 {
274 if (!is_object($this->box) || !isset($this->box->params))
275 {
276 return '';
277 }
278
279 $css = $this->box->params->get('customcss', '');
280
281 if (!is_string($css))
282 {
283 return '';
284 }
285
286 /**
287 * The CSS lands inside an inline <style> element, where a literal "</style>"
288 * would end the element and let markup (e.g. a <script>) escape into the page.
289 * Strip tags the same way core's wp_custom_css_cb() does — legitimate CSS
290 * contains no tags, so this only neuters breakout attempts.
291 */
292 return wp_strip_all_tags($css);
293 }
294
295 /**
296 * Recursively casts an object/array tree to a plain array, without the
297 * json_decode(wp_json_encode()) round-trip.
298 *
299 * @param mixed $data
300 *
301 * @return array
302 */
303 private static function recursiveToArray($data)
304 {
305 if (is_object($data))
306 {
307 $data = get_object_vars($data);
308 }
309
310 if (!is_array($data))
311 {
312 return [];
313 }
314
315 foreach ($data as $key => $value)
316 {
317 if (is_object($value) || is_array($value))
318 {
319 $data[$key] = self::recursiveToArray($value);
320 }
321 }
322
323 return $data;
324 }
325
326 /**
327 * Send a helpful object to JavaScript files
328 *
329 * @return void
330 */
331 public static function setJSObject()
332 {
333 if (self::$loadedLocalizedScript)
334 {
335 return;
336 }
337 self::$loadedLocalizedScript = true;
338
339 /**
340 * The referrer is deliberately not included here. On a full-page-cached site this
341 * HTML is written once and served to everyone, so a baked-in referrer would report
342 * whoever warmed the cache. The runtime reads `document.referrer` instead.
343 */
344 $data = [
345 'ajax_url' => admin_url('admin-ajax.php'),
346 'nonce' => wp_create_nonce('fbox_js_nonce'),
347 'site_url' => site_url('/'),
348
349 // Shown when a form submission never gets a usable response back (network error,
350 // non-JSON body, PHP fatal). The form messages themselves come from the block.
351 'form_submit_failed' => firebox()->_('FB_FORM_SUBMIT_FAILED'),
352
353 /**
354 * The scripts write cookies PHP has to read back, so they need the
355 * same name, path and domain it uses: the prefix keeps the sites of
356 * a subdirectory network apart, the path is the site's own corner of
357 * the domain, and the domain is whatever COOKIE_DOMAIN says.
358 */
359 'cookie_prefix' => \FireBox\Core\Helpers\BoxHelper::cookiePrefix(),
360 'cookie_path' => \FireBox\Core\Helpers\BoxHelper::cookiePath(),
361 'cookie_domain' => \FireBox\Core\Helpers\BoxHelper::cookieDomain()
362 ];
363
364 wp_add_inline_script('firebox-main', 'const fbox_js_object = ' . wp_json_encode($data), 'before');
365 }
366
367 /**
368 * Load Box Media
369 *
370 * @return void
371 */
372 public function loadBoxMedia($box)
373 {
374 $box = new Registry($box);
375
376 $this->loadAnimationsMedia($box);
377
378 /**
379 * FireBox JS
380 */
381 wp_enqueue_script('firebox-main');
382
383
384
385 /**
386 * Add Custom Javascript.
387 *
388 * Emitted verbatim into every visitor's page, so it is gated on the author
389 * holding unfiltered_html — the campaign meta is writable by any
390 * edit_fireboxes user (editor, REST, import) and must not be a way to run
391 * script that user could not write elsewhere.
392 */
393 $custom_code = $box->get('params.data.customcode', '');
394 if (is_string($custom_code) && !empty($custom_code)
395 && \FireBox\Core\Helpers\CustomCode::isAllowedForCampaign($box->get('ID')))
396 {
397 $custom_code = html_entity_decode(stripslashes($custom_code));
398 BoxHelper::addInlineScript($custom_code);
399 }
400
401 // run above the main JS script to run only once
402 self::setJSObject();
403
404 /**
405 * FireBox CSS
406 */
407 wp_enqueue_style(
408 'firebox',
409 FBOX_MEDIA_PUBLIC_URL . 'css/firebox.css',
410 [],
411 FBOX_VERSION
412 );
413
414 /**
415 * Page Slide mode JS
416 */
417 if ($box->get('params.data.mode') == 'pageslide')
418 {
419 // This script moves the campaign instances into the page-slide wrapper after
420 // firebox-main creates them. Deferring is safe: deferred scripts execute in
421 // document order and firebox-main is declared as a dependency, so it always
422 // runs first. All inline scripts on firebox-main are attached in the "before"
423 // position, which keeps its defer strategy intact.
424 wp_enqueue_script(
425 'firebox-pageslide-mode',
426 FBOX_MEDIA_PUBLIC_URL . 'js/pageslide_mode.js',
427 ['firebox-main'],
428 FBOX_VERSION,
429 ['in_footer' => true, 'strategy' => 'defer']
430 );
431 }
432
433
434 }
435
436 /**
437 * Enqueues only the Animate.css animations the campaign actually uses.
438 *
439 * The full library is 71 KB of render-blocking CSS for what is almost always a
440 * single fade, so it is split at build time (gulp build-animations) into one file
441 * per animation plus a shared base. Campaigns rendering on the same page union
442 * their animations naturally, since each gets its own handle.
443 *
444 * @param Registry $box
445 *
446 * @return void
447 */
448 private function loadAnimationsMedia($box)
449 {
450 /**
451 * Escape hatch for campaigns whose custom code applies arbitrary
452 * firebox__animate__* classes that we cannot see server-side.
453 */
454 if (apply_filters('firebox/box/load_full_animations', false))
455 {
456 wp_enqueue_style(
457 'firebox-animations',
458 FBOX_MEDIA_PUBLIC_URL . 'css/vendor/animate.min.css',
459 [],
460 FBOX_VERSION
461 );
462
463 return;
464 }
465
466 $animations = array_filter([
467 $box->get('params.data.animationin', ''),
468 $box->get('params.data.animationout', '')
469 ], 'is_string');
470
471 $animations = array_unique(array_filter(array_map('trim', $animations)));
472
473 if (!$animations)
474 {
475 return;
476 }
477
478 $dir = FBOX_PLUGIN_DIR . 'media/public/css/vendor/animations/';
479 $url = FBOX_MEDIA_PUBLIC_URL . 'css/vendor/animations/';
480
481 $enqueued = false;
482
483 foreach ($animations as $animation)
484 {
485 // Animation names are a fixed camelCase slug set; anything else is either a
486 // stale value or a name we no longer ship, and must not reach the filesystem.
487 if (!preg_match('#^[A-Za-z0-9]+$#', $animation) || !file_exists($dir . $animation . '.css'))
488 {
489 continue;
490 }
491
492 if (!$enqueued)
493 {
494 wp_enqueue_style('firebox-animations-base', $url . 'base.css', [], FBOX_VERSION);
495 $enqueued = true;
496 }
497
498 wp_enqueue_style(
499 'firebox-animation-' . $animation,
500 $url . $animation . '.css',
501 ['firebox-animations-base'],
502 FBOX_VERSION
503 );
504 }
505 }
506
507 /**
508 * Prepares the box before rendering
509 *
510 * @return void
511 */
512 public function prepare()
513 {
514 remove_filter('the_content', 'wptexturize');
515
516 $cParam = BoxHelper::getParams();
517 $cParam = new Registry($cParam);
518
519 $this->box->post_content = apply_filters('the_content', $this->box->post_content);
520
521 $mode = $this->box->params->get('mode');
522
523 /* Classes */
524 $css_class = [
525 $this->box->ID,
526 $mode
527 ];
528
529 if (in_array($mode, ['popup', 'stickybar', 'sidebar', 'floating', 'slide-in']))
530 {
531 $position = $this->box->params->get('position', '');
532 $position = !is_string($position) ? '' : $position;
533 if ($position)
534 {
535 $css_class[] = $position;
536 }
537 }
538 else if ($mode === 'fullscreen')
539 {
540 if ($center_content = $this->box->params->get('center_content', false)) {
541 $css_class[] = 'center-content';
542 }
543 }
544
545 self::prefixCSSClasses($css_class);
546
547 // Class suffix
548 $classSuffix = $this->box->params->get('classsuffix', '');
549 $classSuffix = is_string($classSuffix) ? $classSuffix : '';
550
551 $css_class[] = $classSuffix;
552
553 $this->box->classes = $css_class;
554
555 // Dialog CSS Classes
556 $dialog_css_classes = [
557 // Add Box shadow
558 $this->box->params->get('boxshadow') ? 'shdelevation' : null
559 ];
560
561 // Align Content
562 $aligncontent = is_string($this->box->params->get('aligncontent')) ? explode(' ', $this->box->params->get('aligncontent')) : [];
563 $dialog_css_classes = array_merge($dialog_css_classes, $aligncontent);
564
565 self::prefixCSSClasses($dialog_css_classes);
566 $this->box->dialog_classes = $dialog_css_classes;
567
568 $trigger_point_methods = [
569 'pageload' => 'onPageLoad',
570 'onclick' => 'onClick',
571 'elementHover' => 'onHover',
572 'ondemand' => 'onDemand',
573
574 ];
575
576 /* Other Settings */
577 $this->box->params->set('animation_duration', $this->box->params->get('animation_duration', 0.2));
578
579 $scroll_amount = $this->box->params->get('scroll_amount', '80%');
580
581 // Parse scroll_amount to extract unit and value
582 $scroll_amount_data = $this->parseScrollAmount($scroll_amount);
583
584 $delay = in_array($this->box->params->get('triggermethod'), ['floatingbutton', 'onexternallink']) ? 0 : (int) $this->box->params->get('triggerdelay') * 1000;
585
586 $trigger_method = (is_string($this->box->params->get('triggermethod'))) && array_key_exists($this->box->params->get('triggermethod'), $trigger_point_methods) ? $trigger_point_methods[$this->box->params->get('triggermethod')] : $this->box->params->get('triggermethod');
587
588 $trigger_element = is_scalar($this->box->params->get('triggerelement', '')) ? $this->box->params->get('triggerelement', '') : '';
589
590 // Use Namespaced classes for each trigger point and let them manipulate the settings dynamicaly.
591 $this->box->settings = [
592 'name' => $this->box->post_title,
593 'trigger' => $trigger_method,
594 'trigger_selector' => $trigger_method === 'onExternalLink' ? '' : rtrim($trigger_element, ','),
595 'delay' => $delay,
596
597 'close_on_esc' => (bool) $this->box->params->get('close_on_esc', false),
598 'animation_open' => $this->box->params->get('animationin'),
599 'animation_close' => $this->box->params->get('animationout'),
600 'animation_duration' => (float) $this->box->params->get('animation_duration') * 1000,
601 'prevent_default' => true,
602 'backdrop' => (bool) $this->box->params->get('overlay'),
603 'backdrop_color' => $this->box->params->get('overlay_color'),
604 'backdrop_click' => (bool) $this->box->params->get('overlayclick'),
605 'disable_page_scroll' => (bool) $this->box->params->get('preventpagescroll'),
606 'test_mode' => (bool) $this->box->params->get('testmode'),
607 'debug' => (bool) $cParam->get('debug', false),
608 'auto_focus' => (bool) $this->box->params->get('autofocus', false),
609 'mode' => $this->box->params->get('mode'),
610 // Session-dependent display conditions resolved in the browser (see pass())
611 'client_rules' => $this->clientRuleGroups
612 ];
613
614 $this->css = new Styling\CSS($this->box);
615
616 // Apply Popup CSS
617 $this->box->params->set('customcss', $this->box->params->get('customcss') . $this->css->getCSS());
618
619 $this->replaceBoxSmartTags();
620
621 add_filter('the_content', 'wptexturize');
622 }
623
624 /**
625 * Parses scroll_amount to extract unit and value
626 *
627 * @param mixed $scroll_amount
628 *
629 * @return array
630 */
631 private function parseScrollAmount($scroll_amount)
632 {
633 if (is_array($scroll_amount))
634 {
635 return [
636 'unit' => $scroll_amount['unit'] ?? '%',
637 'value' => $scroll_amount['value'] ?? 80
638 ];
639 }
640
641 if (is_string($scroll_amount) && preg_match('/^(\d+)(px|%)$/', $scroll_amount, $matches))
642 {
643 return [
644 'unit' => $matches[2],
645 'value' => $matches[1]
646 ];
647 }
648
649 return [
650 'unit' => '%',
651 'value' => 80
652 ];
653 }
654
655 /**
656 * Replaces all box smart tags
657 *
658 * @return void
659 */
660 public function replaceBoxSmartTags()
661 {
662 $tags = new \FPFramework\Base\SmartTags\SmartTags();
663
664 // register FB Smart Tags
665 $tags->register('\FireBox\Core\SmartTags', FBOX_BASE_FOLDER . '/Inc/Core/SmartTags', $this->box);
666
667 $this->box = $tags->replace($this->box);
668 }
669
670 /**
671 * Checks if a box passes assignments
672 *
673 * @return boolean
674 */
675 public function pass()
676 {
677 $this->clientRuleGroups = [];
678
679 if (!$this->box || !is_object($this->box))
680 {
681 return false;
682 }
683
684 // Check first local assignments
685 if (!$this->passLocalAssignments())
686 {
687 return false;
688 }
689
690 $displayConditionsType = $this->box->params->get('display_conditions_type', '');
691
692 // If empty, display popup sitewide
693 if (empty($displayConditionsType) || $displayConditionsType === 'all')
694 {
695 return true;
696 }
697
698 // Mirror Display Conditions of another popup.
699 if ($displayConditionsType == 'mirror' && $mirror_box_id = $this->box->params->get('mirror_box'))
700 {
701 $this->box->params->merge(self::getAssignmentsForMirroring($mirror_box_id));
702 }
703
704 // Get a recursive array of all rules
705 $rules = $this->box->params->get('rules', []);
706 $rules = is_string($rules) ? json_decode($rules, true) : self::recursiveToArray($rules);
707
708 // Normalize to an array; an empty or invalid-JSON "rules" string decodes to null.
709 $rules = is_array($rules) ? $rules : [];
710
711 // The "PHP" display condition runs campaign-authored PHP server-side on every
712 // visitor request (Conditions\PHP -> Executer). Evaluate it only when the campaign
713 // author holds firebox_execute_php; otherwise treat the PHP condition as unmet
714 // rather than running it — an "all" group that requires it fails, an "any" group
715 // falls back to its other conditions. If every group depended on PHP, there is
716 // nothing left to match and the campaign does not show. Runs after the mirror merge
717 // above, so it also covers PHP rules pulled in from a mirrored box.
718 if (self::hasPHPRule($rules) && !\FireBox\Core\Helpers\PHPExecution::isAllowedForCampaign($this->box->ID))
719 {
720 $rules = self::withoutPHPRules($rules);
721
722 if (empty($rules))
723 {
724 return false;
725 }
726 }
727
728 // If testmode is enabled disable the User Groups condition
729 if ($this->box->params->get('testmode'))
730 {
731 foreach ($rules as $key => &$group)
732 {
733 if (!isset($group['rules']) || !is_array($group['rules']))
734 {
735 continue;
736 }
737
738 foreach ($group['rules'] as $_key => &$rule)
739 {
740 if (!isset($rule['name']) || empty($rule['name']))
741 {
742 continue;
743 }
744
745 if ($rule['name'] === 'WP\UserGroup')
746 {
747 unset($group['rules'][$_key]);
748 }
749 }
750 }
751 unset($group);
752 }
753
754 // Check framework based conditions. Session-dependent (client-side) rules are
755 // deferred: the box renders hidden and the frontend runtime resolves them.
756 $result = \FPFramework\Base\Conditions\ConditionBuilder::passWithClientRules($rules, $this->factory);
757
758 $this->clientRuleGroups = $result['client_groups'];
759
760 return $result['pass'];
761 }
762
763 /**
764 * Display condition groups deferred to the frontend runtime by pass().
765 *
766 * @return array
767 */
768 public function getClientRuleGroups()
769 {
770 return $this->clientRuleGroups;
771 }
772
773 /**
774 * Whether a single rule is a "PHP" condition that carries code to run.
775 *
776 * The name is matched case-insensitively: PHP class names resolve regardless
777 * of case, so ConditionsHelper::getCondition() would load the PHP condition for
778 * 'php', 'Php', etc. An empty value is ignored: there is nothing to run.
779 *
780 * @param array $rule A single display-condition rule.
781 *
782 * @return bool
783 */
784 private static function isPHPRule($rule)
785 {
786 if (!is_array($rule) || !isset($rule['name']) || !is_string($rule['name']))
787 {
788 return false;
789 }
790
791 if (strtolower(trim($rule['name'])) !== 'php')
792 {
793 return false;
794 }
795
796 // Only a non-empty string value can be executed as PHP.
797 return isset($rule['value']) && is_string($rule['value']) && trim($rule['value']) !== '';
798 }
799
800 /**
801 * Whether any rule in the given groups is a "PHP" condition that carries code.
802 *
803 * @param array $rules Recursive display-condition groups.
804 *
805 * @return bool
806 */
807 private static function hasPHPRule($rules)
808 {
809 if (!is_array($rules))
810 {
811 return false;
812 }
813
814 foreach ($rules as $group)
815 {
816 if (!isset($group['rules']) || !is_array($group['rules']))
817 {
818 continue;
819 }
820
821 foreach ($group['rules'] as $rule)
822 {
823 if (self::isPHPRule($rule))
824 {
825 return true;
826 }
827 }
828 }
829
830 return false;
831 }
832
833 /**
834 * Returns the rule groups with runnable "PHP" conditions treated as unmet, for a
835 * campaign whose author may not run PHP. A group's matching_method decides how:
836 *
837 * - "all" (every rule must pass): the PHP condition can never be satisfied, so the
838 * whole group fails and is dropped.
839 * - "any" (one rule is enough): the PHP condition is removed so it cannot be the
840 * reason the group passes, but the group's other conditions still evaluate. A group
841 * left with no other conditions is dropped.
842 *
843 * Groups without a PHP condition are returned untouched.
844 *
845 * @param array $rules Recursive display-condition groups.
846 *
847 * @return array
848 */
849 private static function withoutPHPRules($rules)
850 {
851 if (!is_array($rules))
852 {
853 return [];
854 }
855
856 $filtered = [];
857
858 foreach ($rules as $group)
859 {
860 if (!isset($group['rules']) || !is_array($group['rules']))
861 {
862 $filtered[] = $group;
863 continue;
864 }
865
866 $kept = [];
867 $hasPHP = false;
868
869 foreach ($group['rules'] as $rule)
870 {
871 if (self::isPHPRule($rule))
872 {
873 $hasPHP = true;
874 continue;
875 }
876
877 $kept[] = $rule;
878 }
879
880 if (!$hasPHP)
881 {
882 $filtered[] = $group;
883 continue;
884 }
885
886 $method = isset($group['matching_method']) ? $group['matching_method'] : 'all';
887
888 // "all": a required PHP condition is unmet, so the group can never pass.
889 if ($method === 'all')
890 {
891 continue;
892 }
893
894 // "any": keep the remaining alternatives, unless PHP was the only one.
895 if (!empty($kept))
896 {
897 $group['rules'] = $kept;
898 $filtered[] = $group;
899 }
900 }
901
902 return $filtered;
903 }
904
905 /**
906 * Check if a box passes local conditions
907 *
908 * @return boolean
909 */
910 private function passLocalAssignments()
911 {
912 $localAssignments = new \FireBox\Core\FB\Assignments($this, $this->factory);
913 return $localAssignments->passAll();
914 }
915
916 /**
917 * Gets assignments of mirrored box
918 *
919 * @param int $box_id
920 *
921 * @return object
922 */
923 private function getAssignmentsForMirroring($box_id)
924 {
925 // Several campaigns commonly mirror the same source, and every campaign is
926 // evaluated on every page view, so resolve each source at most once per request.
927 static $cache = [];
928
929 $box_id = intval($box_id);
930
931 if (array_key_exists($box_id, $cache))
932 {
933 return $cache[$box_id];
934 }
935
936 $cache[$box_id] = null;
937
938 // The published campaigns are already loaded (and their meta primed) for this
939 // request, so read the mirrored campaign from that list instead of querying again.
940 $boxes = BoxHelper::getAllBoxes();
941 $mirrored = null;
942
943 foreach ($boxes->posts as $box)
944 {
945 if ((int) $box->ID === $box_id)
946 {
947 $mirrored = $box;
948 break;
949 }
950 }
951
952 if (!$mirrored)
953 {
954 return $cache[$box_id];
955 }
956
957 // get meta options for box
958 $params = new Registry(BoxHelper::getMeta($mirrored->ID));
959
960 $cache[$box_id] = new Registry(['rules' => $params->get('rules')]);
961
962 return $cache[$box_id];
963 }
964
965 /**
966 * Prefixes the CSS classes
967 *
968 * @param array $classes
969 * @param string $prefix
970 *
971 * @return void
972 */
973 private static function prefixCSSClasses(&$classes, $prefix = 'fb-')
974 {
975 $classes = array_filter($classes);
976
977 if (empty($classes))
978 {
979 return;
980 }
981
982 foreach ($classes as &$class)
983 {
984 $class = $prefix . $class;
985 }
986 }
987
988 /**
989 * Track box open
990 *
991 * @param integer $box_id
992 * @param string $page
993 * @param string $referrer
994 *
995 * @return void
996 */
997 public function logOpenEvent($box_id, $page = null, $referrer = null)
998 {
999 $box = $this->get($box_id);
1000
1001 if (!is_object($box) || !isset($box->params))
1002 {
1003 return;
1004 }
1005
1006 // Do not track if statistics option is disabled
1007 $track_open_event = (bool) (is_null($box->params->get('stats', null)) ? true : $box->params->get('stats'));
1008 if (!$track_open_event)
1009 {
1010 return;
1011 }
1012
1013 return firebox()->log->track($box_id, 1, null, $page, $referrer);
1014 }
1015
1016 /**
1017 * Track box close
1018 *
1019 * @param integer $box_id
1020 * @param integer $box_log_id
1021 *
1022 * @return void
1023 */
1024 public function logCloseEvent($box_id, $box_log_id)
1025 {
1026 $box = $this->get($box_id);
1027
1028 if (!is_object($box) || !isset($box->params))
1029 {
1030 return null;
1031 }
1032
1033 // Do not track if statistics option is disabled
1034 $track_open_event = (bool) (is_null($box->params->get('stats', null)) ? true : $box->params->get('stats'));
1035 if (!$track_open_event)
1036 {
1037 return null;
1038 }
1039
1040 firebox()->log->track($box_id, 2, $box_log_id);
1041 }
1042
1043 /**
1044 * Get total box impressions
1045 *
1046 * @param array $payload
1047 *
1048 * @return array
1049 */
1050 public function getTotalImpressions($payload)
1051 {
1052 // Cached: the same campaign/period is counted once per request even when
1053 // several conditions ask for it.
1054 return firebox()->tables->boxlog->getResults($payload, true, true);
1055 }
1056
1057 /**
1058 * Returns the cookie instance.
1059 *
1060 * @return mixed
1061 */
1062 public function getCookie()
1063 {
1064 if (!$this->box)
1065 {
1066 return;
1067 }
1068
1069 return new Cookie($this->box);
1070 }
1071
1072 /**
1073 * Returns the box.
1074 *
1075 * @return object
1076 */
1077 public function getBox()
1078 {
1079 return $this->box;
1080 }
1081
1082 /**
1083 * Sets the box.
1084 *
1085 * @param object $box
1086 *
1087 * @return Box
1088 */
1089 public function setBox($box)
1090 {
1091 $this->box = $box;
1092
1093 return $this;
1094 }
1095
1096 public function getParams()
1097 {
1098 return $this->params;
1099 }
1100
1101 public function setParams($params)
1102 {
1103 $this->params = $params;
1104
1105 return $this;
1106 }
1107
1108 public function getCampaignParams()
1109 {
1110 return isset($this->box->params) ? $this->box->params : null;
1111 }
1112
1113 public function setCampaignParams($params)
1114 {
1115 if (isset($this->box->params))
1116 {
1117 $this->box->params = $params;
1118 }
1119
1120 return $this;
1121 }
1122 }
1123