# firebox/trunk/Inc/Core/FB/Box.php

FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin &amp; Cart Abandonment, version trunk. 1,123 lines.

- Page: https://pluginprobe.com/plugins/firebox/trunk/code/Inc/Core/FB/Box.php
- Raw: https://pluginprobe.com/plugins/firebox/trunk/raw/Inc/Core/FB/Box.php
- Modified: 2026-09-07T07:37:52+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/firebox/trunk/code/Inc/Core/FB/Box.php#L10-L20`.

```php
<?php
/**
 * @package         FireBox
 * @version         3.1.13 Free
 * 
 * @author          FirePlugins <info@fireplugins.com>
 * @link            https://www.fireplugins.com
 * @copyright       Copyright © 2026 FirePlugins All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace FireBox\Core\FB;

if (!defined('ABSPATH'))
{
	exit; // Exit if accessed directly.
}

use FireBox\Core\Helpers\BoxHelper;
use FPFramework\Libs\Registry;
use FPFramework\Helpers\Fields\DimensionsHelper;
use FPFramework\Helpers\CSS;

class Box
{
	/**
	 * Send useful JS snippet once in first box
	 * 
	 * @var  boolean
	 */
	static $loadedLocalizedScript = false;

	/**
	 * The box.
	 * 
	 * @param   object
	 */
	private $box = null;

	/**
	 * Factory
	 * 
	 * @var  Factory
	 */
	private $factory = null;

	/**
	 * FireBox settings.
	 * 
	 * @var  object
	 */
	private $params = null;

	/**
	 * Popup CSS.
	 *
	 * @var  CSS
	 */
	public $css = null;

	/**
	 * Display condition groups that must be resolved in the browser before the
	 * campaign may open (session-dependent rules like Time on Site/Pageviews).
	 * Populated by pass(), shipped to the frontend runtime via prepare().
	 *
	 * @var  array
	 */
	private $clientRuleGroups = [];

	/**
	 * Constructor.
	 * 
	 * @param   object  $box
	 * @param   object  $factory
	 * 
	 * @return  void
	 */
	public function __construct($box = null, $factory = null)
	{
		if ($box)
		{
			$this->box = $this->prepareConstructorBox($box);
		}

		if (!$factory)
		{
			$factory = new \FPFramework\Base\Factory();
		}
		$this->factory = $factory;

		$this->params = new Registry(BoxHelper::getParams());
	}

	/**
	 * Allow to set either a box ID or box object
	 * and we then set the box object.
	 * 
	 * @param   mixed   $box
	 * 
	 * @return  object
	 */
	private function prepareConstructorBox($box)
	{
		if (!is_object($box))
		{
			$box = $this->get($box);
		}
		
		return $box;
	}

	/**
	 * Get a box.
	 *
	 * @param   int     $id
	 * @param   string  $status
	 *
	 * @return  object|null
	 */
	public function get($id = null, $status = null)
	{
		if (!$id)
		{
			return null;
		}

		$payload = [
			'where' => [
				'ID' => ' = ' . intval($id),
				'post_type' => " = 'firebox'"
			]
		];

		// apply status if given
		if ($status)
		{
			$payload['where']['post_status'] = ' = \'' . sanitize_key($status) . '\'';
		}

		if (!$box = firebox()->tables->box->getResults($payload))
		{
			return null;
		}

		if (!isset($box[0]))
		{
			return null;
		}
		
		$this->box = $box[0];

		// get meta options for box
		$meta = \FireBox\Core\Helpers\BoxHelper::getMeta($id);
		$this->box->params = new Registry($meta);

		return $this->box;
	}

	/**
	 * Renders the box.
	 * 
	 * @return  void
	 */
	public function render()
	{
		// Check Publishing Assignments
        if (!$this->pass())
        {
			return false;
		}

		$fbox = $this->box;

		/**
		 * Runs before rendering the box.
		 */
		$this->box = apply_filters('firebox/box/before_render', $this->box);

		$this->prepare();
		
		$css = $this->getCustomCSS();

		add_action('wp_enqueue_scripts', function() use ($fbox, $css) {
			// Loads all media files.
			$this->loadBoxMedia($fbox);

			// Load CSS
			if ($css)
			{
				wp_add_inline_style('firebox', $css);
			}

			
		});
		
		// Allow to manipulate the box before rendering
		$this->box = apply_filters('firebox/box/edit', $this->box);

		// payload
		$payload = [
			'box' => $this->box,
			'params' => $this->params,
		];

		// print campaign HTML
		add_action('wp_footer', function() use ($payload) {
			echo $this->getFinalCampaignHTML($payload); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		});

		return true;
	}

	public function renderEmbed()
	{
		// Check Publishing Assignments
        if (!$this->pass())
        {
			return false;
		}

		/**
		 * Runs before rendering the box.
		 */
		$this->box = apply_filters('firebox/box/before_render', $this->box);

		$this->prepare();
		
		// Loads all media files.
		$this->loadBoxMedia($this->box);

		$css = $this->getCustomCSS();

		wp_register_style('fireboxStyle', false);
		wp_enqueue_style('fireboxStyle');

		// Load CSS
		if ($css)
		{
			wp_add_inline_style('fireboxStyle', $css);
		}

		
		
		// Allow to manipulate the box before rendering
		$this->box = apply_filters('firebox/box/edit', $this->box);

		// payload
		$payload = [
			'box' => $this->box,
			'params' => $this->params,
		];

		// return box template
		return $this->getFinalCampaignHTML($payload);
	}

	public function getFinalCampaignHTML($payload)
	{
		$html = firebox()->renderer->public->render('box', $payload, true);

		/**
		 * Runs after rendering the box.
		 */
		return apply_filters('firebox/box/after_render', $html, $payload['box']);
	}

	/**
	 * Gets the Custom CSS of the popup.
	 * 
	 * @return  string
	 */
	public function getCustomCSS()
	{
		if (!is_object($this->box) || !isset($this->box->params))
		{
			return '';
		}

		$css = $this->box->params->get('customcss', '');

		if (!is_string($css))
		{
			return '';
		}

		/**
		 * The CSS lands inside an inline <style> element, where a literal "</style>"
		 * would end the element and let markup (e.g. a <script>) escape into the page.
		 * Strip tags the same way core's wp_custom_css_cb() does — legitimate CSS
		 * contains no tags, so this only neuters breakout attempts.
		 */
		return wp_strip_all_tags($css);
	}

	/**
	 * Recursively casts an object/array tree to a plain array, without the
	 * json_decode(wp_json_encode()) round-trip.
	 *
	 * @param   mixed  $data
	 *
	 * @return  array
	 */
	private static function recursiveToArray($data)
	{
		if (is_object($data))
		{
			$data = get_object_vars($data);
		}

		if (!is_array($data))
		{
			return [];
		}

		foreach ($data as $key => $value)
		{
			if (is_object($value) || is_array($value))
			{
				$data[$key] = self::recursiveToArray($value);
			}
		}

		return $data;
	}

	/**
	 * Send a helpful object to JavaScript files
	 *
	 * @return  void
	 */
	public static function setJSObject()
	{
		if (self::$loadedLocalizedScript)
		{
			return;
		}
		self::$loadedLocalizedScript = true;
		
		/**
		 * The referrer is deliberately not included here. On a full-page-cached site this
		 * HTML is written once and served to everyone, so a baked-in referrer would report
		 * whoever warmed the cache. The runtime reads `document.referrer` instead.
		 */
		$data = [
			'ajax_url'	=> admin_url('admin-ajax.php'),
			'nonce'		=> wp_create_nonce('fbox_js_nonce'),
			'site_url'	=> site_url('/'),

			// Shown when a form submission never gets a usable response back (network error,
			// non-JSON body, PHP fatal). The form messages themselves come from the block.
			'form_submit_failed' => firebox()->_('FB_FORM_SUBMIT_FAILED'),

			/**
			 * The scripts write cookies PHP has to read back, so they need the
			 * same name, path and domain it uses: the prefix keeps the sites of
			 * a subdirectory network apart, the path is the site's own corner of
			 * the domain, and the domain is whatever COOKIE_DOMAIN says.
			 */
			'cookie_prefix' => \FireBox\Core\Helpers\BoxHelper::cookiePrefix(),
			'cookie_path'   => \FireBox\Core\Helpers\BoxHelper::cookiePath(),
			'cookie_domain' => \FireBox\Core\Helpers\BoxHelper::cookieDomain()
		];

		wp_add_inline_script('firebox-main', 'const fbox_js_object = ' . wp_json_encode($data), 'before');
	}

	/**
	 * Load Box Media
	 * 
	 * @return  void
	 */
	public function loadBoxMedia($box)
	{
		$box = new Registry($box);

		$this->loadAnimationsMedia($box);

		/**
		 * FireBox JS
		 */
		wp_enqueue_script('firebox-main');

		

		/**
		 * Add Custom Javascript.
		 *
		 * Emitted verbatim into every visitor's page, so it is gated on the author
		 * holding unfiltered_html — the campaign meta is writable by any
		 * edit_fireboxes user (editor, REST, import) and must not be a way to run
		 * script that user could not write elsewhere.
		 */
		$custom_code = $box->get('params.data.customcode', '');
		if (is_string($custom_code) && !empty($custom_code)
			&& \FireBox\Core\Helpers\CustomCode::isAllowedForCampaign($box->get('ID')))
		{
			$custom_code = html_entity_decode(stripslashes($custom_code));
			BoxHelper::addInlineScript($custom_code);
		}

		// run above the main JS script to run only once
        self::setJSObject();
		
		/**
		 * FireBox CSS
		 */
		wp_enqueue_style(
			'firebox',
			FBOX_MEDIA_PUBLIC_URL . 'css/firebox.css',
			[],
			FBOX_VERSION
		);

		/**
		 * Page Slide mode JS
		 */
		if ($box->get('params.data.mode') == 'pageslide')
		{
			// This script moves the campaign instances into the page-slide wrapper after
			// firebox-main creates them. Deferring is safe: deferred scripts execute in
			// document order and firebox-main is declared as a dependency, so it always
			// runs first. All inline scripts on firebox-main are attached in the "before"
			// position, which keeps its defer strategy intact.
			wp_enqueue_script(
				'firebox-pageslide-mode',
				FBOX_MEDIA_PUBLIC_URL . 'js/pageslide_mode.js',
				['firebox-main'],
				FBOX_VERSION,
				['in_footer' => true, 'strategy' => 'defer']
			);
		}

		
	}

	/**
	 * Enqueues only the Animate.css animations the campaign actually uses.
	 *
	 * The full library is 71 KB of render-blocking CSS for what is almost always a
	 * single fade, so it is split at build time (gulp build-animations) into one file
	 * per animation plus a shared base. Campaigns rendering on the same page union
	 * their animations naturally, since each gets its own handle.
	 *
	 * @param   Registry  $box
	 *
	 * @return  void
	 */
	private function loadAnimationsMedia($box)
	{
		/**
		 * Escape hatch for campaigns whose custom code applies arbitrary
		 * firebox__animate__* classes that we cannot see server-side.
		 */
		if (apply_filters('firebox/box/load_full_animations', false))
		{
			wp_enqueue_style(
				'firebox-animations',
				FBOX_MEDIA_PUBLIC_URL . 'css/vendor/animate.min.css',
				[],
				FBOX_VERSION
			);

			return;
		}

		$animations = array_filter([
			$box->get('params.data.animationin', ''),
			$box->get('params.data.animationout', '')
		], 'is_string');

		$animations = array_unique(array_filter(array_map('trim', $animations)));

		if (!$animations)
		{
			return;
		}

		$dir = FBOX_PLUGIN_DIR . 'media/public/css/vendor/animations/';
		$url = FBOX_MEDIA_PUBLIC_URL . 'css/vendor/animations/';

		$enqueued = false;

		foreach ($animations as $animation)
		{
			// Animation names are a fixed camelCase slug set; anything else is either a
			// stale value or a name we no longer ship, and must not reach the filesystem.
			if (!preg_match('#^[A-Za-z0-9]+$#', $animation) || !file_exists($dir . $animation . '.css'))
			{
				continue;
			}

			if (!$enqueued)
			{
				wp_enqueue_style('firebox-animations-base', $url . 'base.css', [], FBOX_VERSION);
				$enqueued = true;
			}

			wp_enqueue_style(
				'firebox-animation-' . $animation,
				$url . $animation . '.css',
				['firebox-animations-base'],
				FBOX_VERSION
			);
		}
	}

	/**
	 * Prepares the box before rendering
	 *
	 * @return  void
	 */
	public function prepare()
	{
		remove_filter('the_content', 'wptexturize');

		$cParam = BoxHelper::getParams();
		$cParam = new Registry($cParam);

		$this->box->post_content = apply_filters('the_content', $this->box->post_content);

		$mode = $this->box->params->get('mode');
		
        /* Classes */
        $css_class = [
            $this->box->ID,
			$mode
		];

		if (in_array($mode, ['popup', 'stickybar', 'sidebar', 'floating', 'slide-in']))
		{
			$position = $this->box->params->get('position', '');
			$position = !is_string($position) ? '' : $position;
			if ($position)
			{
				$css_class[] = $position;
			}
		}
		else if ($mode === 'fullscreen')
		{
			if ($center_content = $this->box->params->get('center_content', false)) {
				$css_class[] = 'center-content';
			}
		}
		
		self::prefixCSSClasses($css_class);
		
		// Class suffix
		$classSuffix = $this->box->params->get('classsuffix', '');
		$classSuffix = is_string($classSuffix) ? $classSuffix : '';
		
        $css_class[] = $classSuffix;
		
		$this->box->classes = $css_class;
		
		// Dialog CSS Classes
        $dialog_css_classes = [
			// Add Box shadow
            $this->box->params->get('boxshadow') ? 'shdelevation' : null
		];

		// Align Content
		$aligncontent = is_string($this->box->params->get('aligncontent')) ? explode(' ', $this->box->params->get('aligncontent')) : [];
        $dialog_css_classes = array_merge($dialog_css_classes, $aligncontent);
		
        self::prefixCSSClasses($dialog_css_classes);
		$this->box->dialog_classes = $dialog_css_classes;
		
        $trigger_point_methods = [
            'pageload'     => 'onPageLoad',
            'onclick'      => 'onClick',
            'elementHover' => 'onHover',
            'ondemand'     => 'onDemand',
			
		];

		/* Other Settings */
		$this->box->params->set('animation_duration', $this->box->params->get('animation_duration', 0.2));

		$scroll_amount = $this->box->params->get('scroll_amount', '80%');

		// Parse scroll_amount to extract unit and value
		$scroll_amount_data = $this->parseScrollAmount($scroll_amount);

		$delay = in_array($this->box->params->get('triggermethod'), ['floatingbutton', 'onexternallink']) ? 0 : (int) $this->box->params->get('triggerdelay') * 1000;

		$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');

		$trigger_element = is_scalar($this->box->params->get('triggerelement', '')) ? $this->box->params->get('triggerelement', '') : '';

        // Use Namespaced classes for each trigger point and let them manipulate the settings dynamicaly.
        $this->box->settings = [
			'name'				   => $this->box->post_title,
            'trigger'              => $trigger_method,
            'trigger_selector'     => $trigger_method === 'onExternalLink' ? '' : rtrim($trigger_element, ','),
            'delay'                => $delay,
			
            'close_on_esc'         => (bool) $this->box->params->get('close_on_esc', false),
            'animation_open'       => $this->box->params->get('animationin'),
            'animation_close'      => $this->box->params->get('animationout'),
			'animation_duration'   => (float) $this->box->params->get('animation_duration') * 1000,
			'prevent_default'      => true,
            'backdrop'             => (bool) $this->box->params->get('overlay'),
            'backdrop_color'       => $this->box->params->get('overlay_color'),
            'backdrop_click'       => (bool) $this->box->params->get('overlayclick'),
            'disable_page_scroll'  => (bool) $this->box->params->get('preventpagescroll'),
            'test_mode'            => (bool) $this->box->params->get('testmode'),
            'debug'                => (bool) $cParam->get('debug', false),
			'auto_focus'		   => (bool) $this->box->params->get('autofocus', false),
			'mode'				   => $this->box->params->get('mode'),
			// Session-dependent display conditions resolved in the browser (see pass())
			'client_rules'         => $this->clientRuleGroups
		];

		$this->css = new Styling\CSS($this->box);

		// Apply Popup CSS
		$this->box->params->set('customcss', $this->box->params->get('customcss') . $this->css->getCSS());

		$this->replaceBoxSmartTags();

		add_filter('the_content', 'wptexturize');
	}

	/**
	 * Parses scroll_amount to extract unit and value
	 * 
	 * @param   mixed  $scroll_amount
	 * 
	 * @return  array
	 */
	private function parseScrollAmount($scroll_amount)
	{
		if (is_array($scroll_amount))
		{
			return [
				'unit' => $scroll_amount['unit'] ?? '%',
				'value' => $scroll_amount['value'] ?? 80
			];
		}

		if (is_string($scroll_amount) && preg_match('/^(\d+)(px|%)$/', $scroll_amount, $matches))
		{
			return [
				'unit' => $matches[2],
				'value' => $matches[1]
			];
		}

		return [
			'unit' => '%',
			'value' => 80
		];
	}

	/**
	 * Replaces all box smart tags
	 *
	 * @return  void
	 */
	public function replaceBoxSmartTags()
	{
		$tags = new \FPFramework\Base\SmartTags\SmartTags();

		// register FB Smart Tags
		$tags->register('\FireBox\Core\SmartTags', FBOX_BASE_FOLDER . '/Inc/Core/SmartTags', $this->box);

		$this->box = $tags->replace($this->box);
	}

	/**
	 * Checks if a box passes assignments
	 * 
	 * @return  boolean
	 */
	public function pass()
    {
        $this->clientRuleGroups = [];

        if (!$this->box || !is_object($this->box))
        {
            return false;
		}

        // Check first local assignments
        if (!$this->passLocalAssignments())
        {
            return false;
        }

        $displayConditionsType = $this->box->params->get('display_conditions_type', '');

        // If empty, display popup sitewide
        if (empty($displayConditionsType) || $displayConditionsType === 'all')
        {
            return true;
        }
		
        // Mirror Display Conditions of another popup.
        if ($displayConditionsType == 'mirror' && $mirror_box_id = $this->box->params->get('mirror_box'))
        {
            $this->box->params->merge(self::getAssignmentsForMirroring($mirror_box_id));
        }

		// Get a recursive array of all rules
		$rules = $this->box->params->get('rules', []);
		$rules = is_string($rules) ? json_decode($rules, true) : self::recursiveToArray($rules);

		// Normalize to an array; an empty or invalid-JSON "rules" string decodes to null.
		$rules = is_array($rules) ? $rules : [];

		// The "PHP" display condition runs campaign-authored PHP server-side on every
		// visitor request (Conditions\PHP -> Executer). Evaluate it only when the campaign
		// author holds firebox_execute_php; otherwise treat the PHP condition as unmet
		// rather than running it — an "all" group that requires it fails, an "any" group
		// falls back to its other conditions. If every group depended on PHP, there is
		// nothing left to match and the campaign does not show. Runs after the mirror merge
		// above, so it also covers PHP rules pulled in from a mirrored box.
		if (self::hasPHPRule($rules) && !\FireBox\Core\Helpers\PHPExecution::isAllowedForCampaign($this->box->ID))
		{
			$rules = self::withoutPHPRules($rules);

			if (empty($rules))
			{
				return false;
			}
		}

		// If testmode is enabled disable the User Groups condition
        if ($this->box->params->get('testmode'))
        {
            foreach ($rules as $key => &$group)
            {
                if (!isset($group['rules']) || !is_array($group['rules']))
                {
                    continue;
                }

                foreach ($group['rules'] as $_key => &$rule)
                {
                    if (!isset($rule['name']) || empty($rule['name']))
                    {
                        continue;
                    }

                    if ($rule['name'] === 'WP\UserGroup')
                    {
                        unset($group['rules'][$_key]);
                    }
                }
            }
            unset($group);
        }

        // Check framework based conditions. Session-dependent (client-side) rules are
        // deferred: the box renders hidden and the frontend runtime resolves them.
        $result = \FPFramework\Base\Conditions\ConditionBuilder::passWithClientRules($rules, $this->factory);

        $this->clientRuleGroups = $result['client_groups'];

        return $result['pass'];
	}

    /**
     * Display condition groups deferred to the frontend runtime by pass().
     *
     * @return  array
     */
    public function getClientRuleGroups()
    {
        return $this->clientRuleGroups;
    }

    /**
     * Whether a single rule is a "PHP" condition that carries code to run.
     *
     * The name is matched case-insensitively: PHP class names resolve regardless
     * of case, so ConditionsHelper::getCondition() would load the PHP condition for
     * 'php', 'Php', etc. An empty value is ignored: there is nothing to run.
     *
     * @param   array  $rule  A single display-condition rule.
     *
     * @return  bool
     */
    private static function isPHPRule($rule)
    {
        if (!is_array($rule) || !isset($rule['name']) || !is_string($rule['name']))
        {
            return false;
        }

        if (strtolower(trim($rule['name'])) !== 'php')
        {
            return false;
        }

        // Only a non-empty string value can be executed as PHP.
        return isset($rule['value']) && is_string($rule['value']) && trim($rule['value']) !== '';
    }

    /**
     * Whether any rule in the given groups is a "PHP" condition that carries code.
     *
     * @param   array  $rules  Recursive display-condition groups.
     *
     * @return  bool
     */
    private static function hasPHPRule($rules)
    {
        if (!is_array($rules))
        {
            return false;
        }

        foreach ($rules as $group)
        {
            if (!isset($group['rules']) || !is_array($group['rules']))
            {
                continue;
            }

            foreach ($group['rules'] as $rule)
            {
                if (self::isPHPRule($rule))
                {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Returns the rule groups with runnable "PHP" conditions treated as unmet, for a
     * campaign whose author may not run PHP. A group's matching_method decides how:
     *
     * - "all" (every rule must pass): the PHP condition can never be satisfied, so the
     *   whole group fails and is dropped.
     * - "any" (one rule is enough): the PHP condition is removed so it cannot be the
     *   reason the group passes, but the group's other conditions still evaluate. A group
     *   left with no other conditions is dropped.
     *
     * Groups without a PHP condition are returned untouched.
     *
     * @param   array  $rules  Recursive display-condition groups.
     *
     * @return  array
     */
    private static function withoutPHPRules($rules)
    {
        if (!is_array($rules))
        {
            return [];
        }

        $filtered = [];

        foreach ($rules as $group)
        {
            if (!isset($group['rules']) || !is_array($group['rules']))
            {
                $filtered[] = $group;
                continue;
            }

            $kept   = [];
            $hasPHP = false;

            foreach ($group['rules'] as $rule)
            {
                if (self::isPHPRule($rule))
                {
                    $hasPHP = true;
                    continue;
                }

                $kept[] = $rule;
            }

            if (!$hasPHP)
            {
                $filtered[] = $group;
                continue;
            }

            $method = isset($group['matching_method']) ? $group['matching_method'] : 'all';

            // "all": a required PHP condition is unmet, so the group can never pass.
            if ($method === 'all')
            {
                continue;
            }

            // "any": keep the remaining alternatives, unless PHP was the only one.
            if (!empty($kept))
            {
                $group['rules'] = $kept;
                $filtered[] = $group;
            }
        }

        return $filtered;
    }

    /**
     * Check if a box passes local conditions
     *
     * @return  boolean
     */
    private function passLocalAssignments()
    {
        $localAssignments = new \FireBox\Core\FB\Assignments($this, $this->factory);
        return $localAssignments->passAll();
    }
	
	/**
	 * Gets assignments of mirrored box
	 * 
	 * @param   int  $box_id
	 * 
	 * @return  object
	 */
	private function getAssignmentsForMirroring($box_id)
    {
		// Several campaigns commonly mirror the same source, and every campaign is
		// evaluated on every page view, so resolve each source at most once per request.
		static $cache = [];

		$box_id = intval($box_id);

		if (array_key_exists($box_id, $cache))
		{
			return $cache[$box_id];
		}

		$cache[$box_id] = null;

		// The published campaigns are already loaded (and their meta primed) for this
		// request, so read the mirrored campaign from that list instead of querying again.
		$boxes = BoxHelper::getAllBoxes();
		$mirrored = null;

		foreach ($boxes->posts as $box)
		{
			if ((int) $box->ID === $box_id)
			{
				$mirrored = $box;
				break;
			}
		}

		if (!$mirrored)
		{
			return $cache[$box_id];
		}

		// get meta options for box
		$params = new Registry(BoxHelper::getMeta($mirrored->ID));

		$cache[$box_id] = new Registry(['rules' => $params->get('rules')]);

		return $cache[$box_id];
    }

	/**
	 * Prefixes the CSS classes
	 * 
	 * @param   array   $classes
	 * @param   string  $prefix
	 * 
	 * @return  void
	 */
    private static function prefixCSSClasses(&$classes, $prefix = 'fb-')
    {
		$classes = array_filter($classes);
		
		if (empty($classes))
		{
			return;
		}

        foreach ($classes as &$class)
        {
            $class = $prefix . $class;
        }
    }

	/**
	 * Track box open
	 * 
	 * @param   integer  $box_id
	 * @param   string   $page
	 * @param   string   $referrer
	 * 
	 * @return  void
	 */
    public function logOpenEvent($box_id, $page = null, $referrer = null)
    {
        $box = $this->get($box_id);

        if (!is_object($box) || !isset($box->params))
        {
            return;
        }

        // Do not track if statistics option is disabled
		$track_open_event = (bool) (is_null($box->params->get('stats', null)) ? true : $box->params->get('stats'));
        if (!$track_open_event)
        {
            return;
        }

        return firebox()->log->track($box_id, 1, null, $page, $referrer);
    }

	/**
	 * Track box close
	 * 
	 * @param   integer  $box_id
	 * @param   integer  $box_log_id
	 * 
	 * @return  void
	 */
    public function logCloseEvent($box_id, $box_log_id)
    {
        $box = $this->get($box_id);

        if (!is_object($box) || !isset($box->params))
        {
            return null;
        }

        // Do not track if statistics option is disabled
		$track_open_event = (bool) (is_null($box->params->get('stats', null)) ? true : $box->params->get('stats'));
        if (!$track_open_event)
        {
            return null;
        }

        firebox()->log->track($box_id, 2, $box_log_id);
	}

	/**
	 * Get total box impressions
	 * 
	 * @param   array  $payload
	 * 
	 * @return  array
	 */
	public function getTotalImpressions($payload)
	{
		// Cached: the same campaign/period is counted once per request even when
		// several conditions ask for it.
		return firebox()->tables->boxlog->getResults($payload, true, true);
	}

	/**
	 * Returns the cookie instance.
	 * 
	 * @return  mixed
	 */
	public function getCookie()
	{
		if (!$this->box)
		{
			return;
		}
		
		return new Cookie($this->box);
	}

	/**
	 * Returns the box.
	 * 
	 * @return  object
	 */
	public function getBox()
	{
		return $this->box;
	}

	/**
	 * Sets the box.
	 * 
	 * @param   object  $box
	 * 
	 * @return  Box
	 */
	public function setBox($box)
	{
		$this->box = $box;

		return $this;
	}

	public function getParams()
	{
		return $this->params;
	}

	public function setParams($params)
	{
		$this->params = $params;

		return $this;
	}

	public function getCampaignParams()
	{
		return isset($this->box->params) ? $this->box->params : null;
	}

	public function setCampaignParams($params)
	{
		if (isset($this->box->params))
		{
			$this->box->params = $params;
		}

		return $this;
	}
}

```
