PluginProbe
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment / 3.1.14
FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment v3.1.14
3.1.14 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 All 123 releases
← All changes | Inc/Core/FB/Box.php +726 -566 1.0.11 → 3.1.14 View file →
@@ -1,12 +1,12 @@
1 1 <?php
2 2 /**
3 3 * @package FireBox
4 - * @version 1.0.11 Free
4 + * @version 3.1.14 Free
5 5 *
6 6 * @author FirePlugins <[email protected]>
7 7 * @link https://www.fireplugins.com
8 - * @copyright Copyright © 2022 FirePlugins All Rights Reserved
8 + * @copyright Copyright © 2026 FirePlugins All Rights Reserved
9 9 * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
10 10 */
11 11
12 12 namespace FireBox\Core\FB;
@@ -18,8 +18,9 @@
18 18
19 19 use FireBox\Core\Helpers\BoxHelper;
20 20 use FPFramework\Libs\Registry;
21 21 use FPFramework\Helpers\Fields\DimensionsHelper;
22 +use FPFramework\Helpers\CSS;
22 23
23 24 class Box
24 25 {
25 26 /**
@@ -29,48 +30,104 @@
29 30 */
30 31 static $loadedLocalizedScript = false;
31 32
32 33 /**
33 - * The box settings
34 + * The box.
35 + *
36 + * @param object
34 37 */
35 - private $boxSettings;
38 + private $box = null;
36 39
37 40 /**
38 - * The factory
41 + * Factory
39 42 *
40 43 * @var Factory
41 44 */
42 - protected $factory;
45 + private $factory = null;
43 46
44 - public function __construct($boxSettings = null, $factory = null)
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)
45 79 {
46 - $this->init($boxSettings, $factory);
47 - }
80 + if ($box)
81 + {
82 + $this->box = $this->prepareConstructorBox($box);
83 + }
48 84
49 - public function init($boxSettings = null, $factory = null)
50 - {
51 85 if (!$factory)
52 86 {
53 87 $factory = new \FPFramework\Base\Factory();
54 88 }
55 -
56 - $this->boxSettings = $boxSettings;
57 89 $this->factory = $factory;
90 +
91 + $this->params = new Registry(BoxHelper::getParams());
58 92 }
59 93
60 94 /**
61 - * Gets published box.
95 + * Allow to set either a box ID or box object
96 + * and we then set the box object.
62 97 *
63 - * @param integer $id
64 - * @param string $status
98 + * @param mixed $box
65 99 *
66 - * @return array
100 + * @return object
67 101 */
68 - public function get($id, $status = null)
102 + private function prepareConstructorBox($box)
69 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 +
70 127 $payload = [
71 128 'where' => [
72 - 'ID' => ' = ' . esc_sql($id),
129 + 'ID' => ' = ' . intval($id),
73 130 'post_type' => " = 'firebox'"
74 131 ]
75 132 ];
76 133
@@ -76,168 +133,197 @@
76 133
77 134 // apply status if given
78 135 if ($status)
79 136 {
80 - $payload['where']['post_status'] = ' = \'' . esc_sql($status) . '\'';
137 + $payload['where']['post_status'] = ' = \'' . sanitize_key($status) . '\'';
81 138 }
82 -
139 +
83 140 if (!$box = firebox()->tables->box->getResults($payload))
84 141 {
85 - return [];
142 + return null;
86 143 }
87 144
88 145 if (!isset($box[0]))
89 146 {
90 - return [];
147 + return null;
91 148 }
92 149
93 - $box = $box[0];
150 + $this->box = $box[0];
94 151
95 152 // get meta options for box
96 - $meta = self::getMeta($id);
97 - $box->params = new Registry($meta);
153 + $meta = \FireBox\Core\Helpers\BoxHelper::getMeta($id);
154 + $this->box->params = new Registry($meta);
98 155
99 - return $box;
156 + return $this->box;
100 157 }
101 158
102 159 /**
103 - * Get box meta
160 + * Renders the box.
104 161 *
105 - * @param int $id
106 - *
107 - * @return array
162 + * @return void
108 163 */
109 - public static function getMeta($id)
164 + public function render()
110 165 {
111 - return get_post_meta($id, 'fpframework_meta_settings', true);
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;
112 211 }
113 212
114 - /**
115 - * Render box
116 - *
117 - * @return string
118 - */
119 - public function render($box = null)
213 + public function renderEmbed()
120 214 {
121 - // we do not have a passed box or box settings from constructor, abort
122 - // as we cannot render a box without any box parameters
123 - if (!$box && !$this->boxSettings)
124 - {
125 - return;
215 + // Check Publishing Assignments
216 + if (!$this->pass())
217 + {
218 + return false;
126 219 }
127 220
128 - // if a box was given to the function, use these settings instead
129 - if ($box)
130 - {
131 - $this->boxSettings = $box;
132 - }
221 + /**
222 + * Runs before rendering the box.
223 + */
224 + $this->box = apply_filters('firebox/box/before_render', $this->box);
133 225
134 - // if we have box settings, then set up our box with these settings
135 - if ($this->boxSettings)
136 - {
137 - $box = $this->boxSettings;
138 - }
226 + $this->prepare();
227 +
228 + // Loads all media files.
229 + $this->loadBoxMedia($this->box);
139 230
140 - // Check Publishing Assignments
141 - if (!$this->pass($box))
142 - {
143 - return;
144 - }
231 + $css = $this->getCustomCSS();
145 232
146 - // get settings params
147 - $params = BoxHelper::getParams();
233 + wp_register_style('fireboxStyle', false);
234 + wp_enqueue_style('fireboxStyle');
148 235
149 - if (!did_action('wp_enqueue_scripts'))
236 + // Load CSS
237 + if ($css)
150 238 {
151 - /**
152 - * Loads all media files.
153 - */
154 - add_action('wp_enqueue_scripts', function() use ($box, $params) {
155 - // load box media
156 - $this->loadBoxMedia($box, $params);
157 - });
239 + wp_add_inline_style('fireboxStyle', $css);
158 240 }
159 - else
160 - {
161 - // load box media
162 - $this->loadBoxMedia($box, $params);
163 - }
164 241
165 - self::prepare($box);
166 -
167 - if (!did_action('wp_head'))
168 - {
169 - /**
170 - * Custom CSS
171 - */
172 - add_action('wp_head', function() use ($box) {
173 - if ($css = $this->getCustomCSS($box))
174 - {
175 - echo '<style type="text/css" class="fb-' . esc_attr($box->ID) . '-style fb-responsive-style">' . $css . '</style>';
176 - }
177 - });
178 - }
179 - else
180 - {
181 - if ($css = $this->getCustomCSS($box))
182 - {
183 - echo '<style type="text/css" class="fb-' . esc_attr($box->ID) . '-style fb-responsive-style">' . $css . '</style>';
184 - }
185 - }
186 242
187 - /**
188 - * Runs before rendering the box.
189 - */
190 - do_action('firebox/box/before_render', $box);
243 +
244 + // Allow to manipulate the box before rendering
245 + $this->box = apply_filters('firebox/box/edit', $this->box);
191 246
192 - // after we prepare it, update box settings
193 - $this->boxSettings = $box;
194 -
195 247 // payload
196 248 $payload = [
197 - 'box' => $box,
198 - 'params' => $params,
249 + 'box' => $this->box,
250 + 'params' => $this->params,
199 251 ];
200 -
252 +
201 253 // return box template
202 - add_action('wp_footer', function() use ($box, $payload) {
203 - echo firebox()->renderer->public->render('box', $payload, true);
204 - });
254 + return $this->getFinalCampaignHTML($payload);
205 255 }
206 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 +
207 267 /**
208 268 * Gets the Custom CSS of the popup.
209 269 *
210 - * @param object $box
211 - *
212 270 * @return string
213 271 */
214 - private function getCustomCSS($box)
272 + public function getCustomCSS()
215 273 {
216 - $css = is_string($box->params->get('customcss')) ? $box->params->get('customcss') : '';
274 + if (!is_object($this->box) || !isset($this->box->params))
275 + {
276 + return '';
277 + }
217 278
218 - // add responsive CSS
219 - if ($responsive_css = (array) $box->params->get('responsive_css', []))
279 + $css = $this->box->params->get('customcss', '');
280 +
281 + if (!is_string($css))
220 282 {
221 - if (isset($responsive_css['tablet']) && !empty($responsive_css['tablet']))
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))
222 318 {
223 - $css .= '
224 -@media only screen and (min-width: 468px) and (max-width: 768px) {
225 - .fb-' . esc_attr($box->ID) . ' .fb-dialog { ' . esc_html($responsive_css['tablet']) . ' }
226 -}';
319 + $data[$key] = self::recursiveToArray($value);
227 320 }
228 - if (isset($responsive_css['mobile']) && !empty($responsive_css['mobile']))
229 - {
230 - $css .= '
231 -@media only screen and (max-width: 468px) {
232 - .fb-' . esc_attr($box->ID) . ' .fb-dialog { ' . esc_html($responsive_css['mobile']) . ' }
233 -}';
234 - }
235 321 }
236 322
237 - return $css;
323 + return $data;
238 324 }
239 -
325 +
240 326 /**
241 327 * Send a helpful object to JavaScript files
242 328 *
243 329 * @return void
@@ -249,99 +335,70 @@
249 335 return;
250 336 }
251 337 self::$loadedLocalizedScript = true;
252 338
253 - $data = array(
254 - 'ajax_url' => admin_url('admin-ajax.php'),
255 - 'nonce' => wp_create_nonce('fbox_js_nonce'),
256 - 'site_url' => site_url('/'),
257 - 'referrer' => wp_get_referer()
258 - );
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('/'),
259 348
260 - wp_localize_script('firebox', 'fbox_js_object', $data);
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');
261 365 }
262 366
263 367 /**
264 368 * Load Box Media
265 369 *
266 - * @param array $box
267 - * @param array $params
268 - *
269 370 * @return void
270 371 */
271 - public function loadBoxMedia($box, $params)
372 + public function loadBoxMedia($box)
272 373 {
273 374 $box = new Registry($box);
274 - $params = new Registry($params);
275 375
376 + $this->loadAnimationsMedia($box);
377 +
276 378 /**
277 - * We require a CSS handle to register inline styles.
278 - *
279 - * For this case, we use this dummy style to add inline CSS for our popups
280 - * even though the FireBox CSS file may not appear in the page.
379 + * FireBox JS
281 380 */
282 - wp_register_style('firebox-custom-styles', false);
283 - wp_enqueue_style('firebox-custom-styles');
381 + wp_enqueue_script('firebox-main');
284 382
285 - // Add polyfills for Internet Explorer
286 - $browser = $this->factory->getBrowser();
287 - if ($browser && is_array($browser) && array_key_exists('name', $browser) && $browser['name'] == 'ie')
288 - {
289 - wp_enqueue_script(
290 - 'firebox-ie11-polyfill',
291 - 'https://polyfill.io/v3/polyfill.min.js?features=NodeList.prototype.forEach%2CElement.prototype.closest%2CArray.prototype.forEach%2CArray.prototype.find%2CIntersectionObserver%2CIntersectionObserverEntry',
292 - [],
293 - FBOX_VERSION,
294 - false
295 - );
296 - }
383 +
297 384
298 385 /**
299 - * Velocity
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.
300 392 */
301 - if ($params->get('loadVelocity', true))
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')))
302 396 {
303 - wp_enqueue_script(
304 - 'firebox-velocity',
305 - FBOX_MEDIA_PUBLIC_URL . 'js/vendor/velocity.js',
306 - [],
307 - FBOX_VERSION,
308 - false
309 - );
310 - wp_enqueue_script(
311 - 'firebox-velocity-ui',
312 - FBOX_MEDIA_PUBLIC_URL . 'js/vendor/velocity.ui.js',
313 - [],
314 - FBOX_VERSION,
315 - false
316 - );
317 -
318 - /**
319 - * Animations
320 - */
321 - if (strpos($box->get('params.data.animationin'), 'firebox') !== false || strpos($box->get('params.data.animationout'), 'firebox') !== false)
322 - {
323 - wp_enqueue_script(
324 - 'firebox-animations',
325 - FBOX_MEDIA_PUBLIC_URL . 'js/animations.js',
326 - [],
327 - FBOX_VERSION,
328 - false
329 - );
330 - }
397 + $custom_code = html_entity_decode(stripslashes($custom_code));
398 + BoxHelper::addInlineScript($custom_code);
331 399 }
332 400
333 - /**
334 - * FireBox JS
335 - */
336 - wp_enqueue_script(
337 - 'firebox',
338 - FBOX_MEDIA_PUBLIC_URL . 'js/firebox.js',
339 - [],
340 - FBOX_VERSION,
341 - false
342 - );
343 -
344 401 // run above the main JS script to run only once
345 402 self::setJSObject();
346 403
347 404 /**
@@ -346,18 +403,14 @@
346 403
347 404 /**
348 405 * FireBox CSS
349 406 */
350 - if ($params->get('loadCSS', true))
351 - {
352 - wp_enqueue_style(
353 - 'firebox',
354 - FBOX_MEDIA_PUBLIC_URL . 'css/firebox.css',
355 - [],
356 - FBOX_VERSION,
357 - false
358 - );
359 - }
407 + wp_enqueue_style(
408 + 'firebox',
409 + FBOX_MEDIA_PUBLIC_URL . 'css/firebox.css',
410 + [],
411 + FBOX_VERSION
412 + );
360 413
361 414 /**
362 415 * Page Slide mode JS
363 416 */
@@ -362,418 +415,500 @@
362 415 * Page Slide mode JS
363 416 */
364 417 if ($box->get('params.data.mode') == 'pageslide')
365 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.
366 424 wp_enqueue_script(
367 425 'firebox-pageslide-mode',
368 426 FBOX_MEDIA_PUBLIC_URL . 'js/pageslide_mode.js',
369 - [],
427 + ['firebox-main'],
370 428 FBOX_VERSION,
371 - false
429 + ['in_footer' => true, 'strategy' => 'defer']
372 430 );
373 431 }
374 432
375 433
376 -
377 - $this->loadThemeCSSOverrides();
378 434 }
379 435
380 436 /**
381 - * Some themes require overrides to preserve as much as we can the styling of the popups.
382 - *
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 + *
383 446 * @return void
384 447 */
385 - private function loadThemeCSSOverrides()
448 + private function loadAnimationsMedia($box)
386 449 {
387 - $active_theme = wp_get_theme();
388 - $theme = $active_theme->template;
389 -
390 450 /**
391 - * This is the listed of the themes that we have created overrides
451 + * Escape hatch for campaigns whose custom code applies arbitrary
452 + * firebox__animate__* classes that we cannot see server-side.
392 453 */
393 - $themes = [
394 - 'twentytwentyone'
395 - ];
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 + );
396 462
397 - if (!in_array($theme, $themes))
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)
398 474 {
399 475 return;
400 476 }
401 477
402 - wp_enqueue_style(
403 - 'firebox-theme-' . $theme . '-override',
404 - FBOX_MEDIA_PUBLIC_URL . 'css/themes/' . $theme . '.css',
405 - [],
406 - FBOX_VERSION,
407 - false
408 - );
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 + }
409 505 }
410 506
411 507 /**
412 508 * Prepares the box before rendering
413 - *
414 - * @param array $box
415 - *
509 + *
416 510 * @return void
417 511 */
418 - public static function prepare(&$box)
512 + public function prepare()
419 513 {
420 - global $post;
421 - $original_post = $post;
514 + remove_filter('the_content', 'wptexturize');
422 515
423 - $GLOBALS['post'] = $box;
424 - setup_postdata($GLOBALS['post']);
425 -
426 516 $cParam = BoxHelper::getParams();
427 517 $cParam = new Registry($cParam);
428 518
429 - $box->post_content = apply_filters('the_content', $box->post_content);
519 + $this->box->post_content = apply_filters('the_content', $this->box->post_content);
430 520
431 - $css_class_prefix = 'fb-';
521 + $mode = $this->box->params->get('mode');
432 522
433 - $position = $box->params->get('position', '');
434 - $position = !is_string($position) ? '' : $position;
435 -
436 523 /* Classes */
437 524 $css_class = [
438 - $box->ID,
439 - $position
525 + $this->box->ID,
526 + $mode
440 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 + }
441 544
442 - $rtl = $box->params->get('rtl', '0');
443 - if ($rtl == '1')
444 - {
445 - $css_class[] = 'rtl';
446 - }
447 -
448 545 self::prefixCSSClasses($css_class);
449 546
450 - // class suffix
451 - $classSuffix = $box->params->get('classsuffix', '');
547 + // Class suffix
548 + $classSuffix = $this->box->params->get('classsuffix', '');
452 549 $classSuffix = is_string($classSuffix) ? $classSuffix : '';
453 550
454 551 $css_class[] = $classSuffix;
455 552
456 - $box->classes = $css_class;
553 + $this->box->classes = $css_class;
457 554
458 - // box shadow
459 - $boxshadow = (is_string($box->params->get('boxshadow', '1')) || is_int($box->params->get('boxshadow', '1'))) ? $box->params->get('boxshadow', '1') : '0';
460 -
555 + // Dialog CSS Classes
461 556 $dialog_css_classes = [
462 - $boxshadow != '0' ? 'shd' . $boxshadow : null
557 + // Add Box shadow
558 + $this->box->params->get('boxshadow') ? 'shdelevation' : null
463 559 ];
464 560
465 561 // Align Content
466 - $aligncontent = is_string($box->params->get('aligncontent')) ? explode(' ', $box->params->get('aligncontent')) : [];
562 + $aligncontent = is_string($this->box->params->get('aligncontent')) ? explode(' ', $this->box->params->get('aligncontent')) : [];
467 563 $dialog_css_classes = array_merge($dialog_css_classes, $aligncontent);
468 564
469 565 self::prefixCSSClasses($dialog_css_classes);
470 - $box->dialog_classes = $dialog_css_classes;
566 + $this->box->dialog_classes = $dialog_css_classes;
471 567
472 - /* CSS */
473 - // border
474 - $border = is_string($box->params->get('bordertype', 'none')) ? $box->params->get('bordertype', 'none') : 'none';
475 - $border_width = $box->params->get('borderwidth.value', 0) ? $box->params->get('borderwidth.value', 0) : 0;
476 - $border_unit = is_string($box->params->get('borderwidth.unit', 'px')) ? $box->params->get('borderwidth.unit', 'px') : 0;
477 - $border_color = is_string($box->params->get('bordercolor')) ? $box->params->get('bordercolor') : '';
478 -
479 - $style = [
480 - 'background' => $box->params->get('backgroundcolor'),
481 - 'color' => $box->params->get('textcolor'),
482 - 'border' => $border == 'none' ? null : $border . ' ' . $border_width . $border_unit . ' ' . $border_color,
483 - ];
484 -
485 - // add shared properties
486 - $shared_properties = [
487 - 'width' => [
488 - 'prefix_only' => true
489 - ],
490 - 'height' => [
491 - 'prefix_only' => true
492 - ],
493 - 'padding' => [],
494 - 'margin' => []
495 - ];
496 - foreach ($shared_properties as $prop => $prop_data)
497 - {
498 - // add dekstop property
499 - $value = (array) $box->params->get($prop . '_control.' . $prop, []);
500 - if (isset($value['desktop']))
501 - {
502 - $prefix_only = isset($prop_data['prefix_only']) ? (bool) $prop_data['prefix_only'] : false;
503 -
504 - $desktop_value = DimensionsHelper::parseDimensionsData($value['desktop'], $prop, false, $prefix_only);
505 - $style = array_merge($style, $desktop_value);
506 - }
507 - }
508 -
509 - // add dekstop border_radius
510 - $border_radius = (array) $box->params->get('borderradius_control.borderradius', []);
511 - if (isset($border_radius['desktop']))
512 - {
513 - $desktop_border_radius = DimensionsHelper::parseDimensionsBorderRadiusData($border_radius['desktop']);
514 - $style = array_merge($style, $desktop_border_radius);
515 - }
516 -
517 - // Background Image
518 - if ($box->params->get('bgimage', false))
519 - {
520 - $bgrepeat = is_string($box->params->get('bgrepeat')) ? $box->params->get('bgrepeat') : '';
521 - $bgsize = is_string($box->params->get('bgsize')) ? $box->params->get('bgsize') : '';
522 - $bgposition = is_string($box->params->get('bgposition')) ? $box->params->get('bgposition') : '';
523 -
524 - $style['background-image'] = 'url(\'' . esc_url($box->params->get('bgimagefile')) . '\')';
525 - $style['background-repeat'] = strtolower($bgrepeat);
526 - $style['background-size'] = strtolower($bgsize);
527 - $style['background-position'] = strtolower($bgposition);
528 - }
529 -
530 - $box->style = BoxHelper::arrayToCSSS($style);
531 -
532 568 $trigger_point_methods = [
533 - 'pageheight' => 'onScrollDepth',
534 - 'element' => 'onElementVisibility',
535 - 'pageready' => 'onPageReady',
536 569 'pageload' => 'onPageLoad',
537 - 'userleave' => 'onExit',
538 570 'onclick' => 'onClick',
539 571 'elementHover' => 'onHover',
540 - 'ondemand' => 'onDemand'
572 + 'ondemand' => 'onDemand',
573 +
541 574 ];
542 575
543 576 /* Other Settings */
544 - $scroll_depth = $box->params->get('scroll_depth', 'percentage');
545 - $scroll_depth = is_string($scroll_depth) ? $scroll_depth : '';
577 + $this->box->params->set('animation_duration', $this->box->params->get('animation_duration', 0.2));
546 578
547 - $animation_duration = $box->params->get('duration') ? (float) $box->params->get('duration') : 0;
548 -
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 +
549 590 // Use Namespaced classes for each trigger point and let them manipulate the settings dynamicaly.
550 - $box->settings = [
551 - 'trigger' => (is_string($box->params->get('triggermethod'))) && array_key_exists($box->params->get('triggermethod'), $trigger_point_methods) ? $trigger_point_methods[$box->params->get('triggermethod')] : $box->params->get('triggermethod'),
552 - 'trigger_selector' => $box->params->get('triggerelement'),
553 - 'delay' => (int) $box->params->get('triggerdelay') * 1000,
554 - 'scroll_depth' => $scroll_depth,
555 - 'scroll_depth_value' => $scroll_depth == 'percentage' ? (int) $box->params->get('triggerpercentage') : (int) $box->params->get('scroll_pixel'),
556 - 'firing_frequency' => (int) $box->params->get('firing_frequency', 1),
557 - 'reverse_scroll_close' => (bool) $box->params->get('autohide'),
558 - 'threshold' => (float) $box->params->get('threshold', 0) / 100,
559 - 'close_out_viewport' => (bool) $box->params->get('close_out_viewport', false),
560 - 'exit_timer' => (int) $box->params->get('exittimer') * 1000,
561 - 'idle_time' => (int) $box->params->get('idle_time') * 1000,
562 - 'animation_open' => $box->params->get('animationin'),
563 - 'animation_close' => $box->params->get('animationout'),
564 - 'animation_duration' => (float) $animation_duration * 1000,
565 - 'prevent_default' => (bool) $box->params->get('preventdefault', true),
566 - 'backdrop' => (bool) $box->params->get('overlay'),
567 - 'backdrop_color' => $box->params->get('overlay_color'),
568 - 'backdrop_click' => (bool) $box->params->get('overlayclick'),
569 - 'disable_page_scroll' => (bool) $box->params->get('preventpagescroll'),
570 - 'test_mode' => (bool) $box->params->get('testmode'),
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'),
571 607 'debug' => (bool) $cParam->get('debug', false),
572 - 'ga_tracking' => (bool) $cParam->get('gaTrack', 0),
573 - 'ga_tracking_id' => $cParam->get('gaID', 0),
574 - 'ga_tracking_label' => $cParam->get('gaCategory'),
575 - 'auto_focus' => (bool) $box->params->get('autofocus', 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
576 612 ];
577 613
578 - $box->styles_container = BoxHelper::arrayToCSSS([
579 - 'z-index' => $box->params->get('zindex', 99999) != 99999 ? $box->params->get('zindex') : null
580 - ]);
614 + $this->css = new Styling\CSS($this->box);
581 615
582 - // Let's start using CSS vars for each box settings.
583 - $cssVars = [
584 - 'animation_duration' => (float) $animation_duration . 's'
585 - ];
586 -
587 - // Enqueue firebox custom inline CSS
588 - $cssVarsForm = self::cssVarsToString($cssVars, '.fb-' . $box->ID);
589 - if (!did_action('wp_enqueue_scripts'))
590 - {
591 - add_action('wp_enqueue_scripts', function() use ($cssVarsForm) {
592 - wp_add_inline_style('firebox-custom-styles', $cssVarsForm);
593 - });
594 - }
595 - else
596 - {
597 - wp_add_inline_style('firebox-custom-styles', $cssVarsForm);
598 - }
616 + // Apply Popup CSS
617 + $this->box->params->set('customcss', $this->box->params->get('customcss') . $this->css->getCSS());
599 618
600 - // set padding and margin for other devices as well
601 - self::setResponsiveCSS($box);
619 + $this->replaceBoxSmartTags();
602 620
603 - self::replaceBoxSmartTags($box);
604 -
605 - $GLOBALS['post'] = $original_post;
606 - wp_reset_postdata();
621 + add_filter('the_content', 'wptexturize');
607 622 }
608 623
609 624 /**
610 - * Converts an array of CSS variables to its corresponding strings.
625 + * Parses scroll_amount to extract unit and value
611 626 *
612 - * @param array $cssVars
613 - * @param string $namespace
627 + * @param mixed $scroll_amount
614 628 *
615 - * @return string
629 + * @return array
616 630 */
617 - private static function cssVarsToString($cssVars, $namespace)
618 - {
619 - $output = '';
620 -
621 - foreach ($cssVars as $key => $value)
622 - {
623 - $output .= '--' . $key . ': ' . $value . ';' . "\n";
624 - }
625 -
626 - return $namespace . ' {
627 - ' . $output . '
628 - }
629 - ';
630 - }
631 -
632 - /**
633 - * Sets all responsive CSS to use in box view
634 - *
635 - * @param object $box
636 - *
637 - * @return void
638 - */
639 - private static function setResponsiveCSS(&$box)
631 + private function parseScrollAmount($scroll_amount)
640 632 {
641 - $tablet = '';
642 - $mobile = '';
643 -
644 - /**
645 - * Handle width, height, padding, margin the same way.
646 - */
647 - $shared_properties = [
648 - 'width' => [
649 - 'prefix_only' => true
650 - ],
651 - 'height' => [
652 - 'prefix_only' => true
653 - ],
654 - 'padding' => [],
655 - 'margin' => []
656 - ];
657 -
658 - foreach ($shared_properties as $prop => $prop_settings)
633 + if (is_array($scroll_amount))
659 634 {
660 - $prop_data = (array) $box->params->get($prop . '_control.' . $prop, []);
661 -
662 - $prefix_only = isset($prop_settings['prefix_only']) ? $prop_settings['prefix_only'] : false;
663 -
664 - // assign each device CSS
665 - if (isset($prop_data['tablet']))
666 - {
667 - $tablet .= BoxHelper::arrayToCSSS(DimensionsHelper::parseDimensionsData($prop_data['tablet'], $prop, true, $prefix_only));
668 - }
669 - if (isset($prop_data['mobile']))
670 - {
671 - $mobile .= BoxHelper::arrayToCSSS(DimensionsHelper::parseDimensionsData($prop_data['mobile'], $prop, true, $prefix_only));
672 - }
635 + return [
636 + 'unit' => $scroll_amount['unit'] ?? '%',
637 + 'value' => $scroll_amount['value'] ?? 80
638 + ];
673 639 }
674 640
675 - // border radius requires a special method
676 - $border_radius = (array) $box->params->get('borderradius_control.borderradius', []);
677 - if (isset($border_radius['tablet']))
641 + if (is_string($scroll_amount) && preg_match('/^(\d+)(px|%)$/', $scroll_amount, $matches))
678 642 {
679 - $tablet .= BoxHelper::arrayToCSSS(DimensionsHelper::parseDimensionsBorderRadiusData($border_radius['tablet'], true));
643 + return [
644 + 'unit' => $matches[2],
645 + 'value' => $matches[1]
646 + ];
680 647 }
681 - if (isset($border_radius['mobile']))
682 - {
683 - $mobile .= BoxHelper::arrayToCSSS(DimensionsHelper::parseDimensionsBorderRadiusData($border_radius['mobile'], true));
684 - }
685 648
686 - $responsive_css = [
687 - 'tablet' => $tablet,
688 - 'mobile' => $mobile
649 + return [
650 + 'unit' => '%',
651 + 'value' => 80
689 652 ];
690 - $box->params->set('responsive_css', $responsive_css);
691 653 }
692 -
654 +
693 655 /**
694 656 * Replaces all box smart tags
695 - *
696 - * @param object $box
697 - *
698 - * @return object
657 + *
658 + * @return void
699 659 */
700 - public static function replaceBoxSmartTags(&$box)
660 + public function replaceBoxSmartTags()
701 661 {
702 662 $tags = new \FPFramework\Base\SmartTags\SmartTags();
703 663
704 664 // register FB Smart Tags
705 - $tags->register('\FireBox\Core\SmartTags', FBOX_BASE_FOLDER . '/Inc/Core/SmartTags', $box);
665 + $tags->register('\FireBox\Core\SmartTags', FBOX_BASE_FOLDER . '/Inc/Core/SmartTags', $this->box);
706 666
707 - $box = $tags->replace($box);
667 + $this->box = $tags->replace($this->box);
708 668 }
709 669
710 670 /**
711 671 * Checks if a box passes assignments
712 672 *
713 - * @param object $box
714 - *
715 673 * @return boolean
716 674 */
717 - public function pass($box)
675 + public function pass()
718 676 {
719 - if (!$box || !is_object($box))
677 + $this->clientRuleGroups = [];
678 +
679 + if (!$this->box || !is_object($this->box))
720 680 {
721 681 return false;
722 682 }
723 683
724 - // set box settings if empty
725 - if (empty($this->boxSettings))
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))
726 719 {
727 - $this->boxSettings = $box;
720 + $rules = self::withoutPHPRules($rules);
721 +
722 + if (empty($rules))
723 + {
724 + return false;
725 + }
728 726 }
729 727
730 - // Prepare boxes that mirror other boxes assignments
731 - if ($box->params->get('mirror', false) && $mirror_box_id = $box->params->get('mirror_box'))
728 + // If testmode is enabled disable the User Groups condition
729 + if ($this->box->params->get('testmode'))
732 730 {
733 - $box->params->merge(self::getAssignmentsForMirroring($mirror_box_id));
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);
734 752 }
735 753
736 - // Check first local assignments
737 - if (!$this->passLocalAssignments($box))
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']))
738 787 {
739 788 return false;
740 789 }
741 790
742 - // If testmode is enabled disable the User Groups assignment
743 - if ($box->params->get('testmode'))
791 + if (strtolower(trim($rule['name'])) !== 'php')
744 792 {
745 - $box->params->set('assignments.assign_grouplevel.selection', '0');
746 - }
747 -
748 - $globalAssignments = $this->passGlobalAssignments($box);
749 -
750 - // Check framework based assignments
751 - return $globalAssignments;
752 - }
793 + return false;
794 + }
753 795
754 - /**
755 - * Passes framework based global assignments
756 - *
757 - * @param object $box
758 - *
759 - * @return boolean
760 - */
761 - private function passGlobalAssignments($box)
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)
762 808 {
763 - $assignments = new \FPFramework\Base\Assignments($this->factory);
764 - $pass = $assignments->passAll($box, $box->params->get('assignmentMatchingMethod', 'and'));
765 - return $pass;
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;
766 831 }
767 832
768 833 /**
769 - * Check if a box passes local assignments
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:
770 836 *
771 - * @param object $box
772 - *
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 + *
773 908 * @return boolean
774 909 */
775 - private function passLocalAssignments($box)
910 + private function passLocalAssignments()
776 911 {
777 912 $localAssignments = new \FireBox\Core\FB\Assignments($this, $this->factory);
778 913 return $localAssignments->passAll();
779 914 }
@@ -784,49 +919,48 @@
784 919 * @param int $box_id
785 920 *
786 921 * @return object
787 922 */
788 - private static function getAssignmentsForMirroring($box_id)
789 - {
790 - $payload = [
791 - 'where' => [
792 - 'ID' => ' = ' . intval($box_id),
793 - 'post_status' => " = 'publish'",
794 - 'post_type' => " = 'firebox'"
795 - ]
796 - ];
797 -
798 - // Load box
799 - if (!$box = firebox()->tables->box->getResults($payload))
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))
800 932 {
801 - return;
933 + return $cache[$box_id];
802 934 }
803 -
804 - $box = $box[0];
805 -
806 - // get meta options for box
807 - $meta = get_post_meta($box_id, 'fpframework_meta_settings', true);
808 - $box->params = $meta;
809 935
810 - // To prevent user frustration, we ignore the following assignments because they are not displayed in the Publishing Assignments.
811 - $params_to_ignore = [
812 - 'assign_impressions'
813 - ];
936 + $cache[$box_id] = null;
814 937
815 - $assignments = [];
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;
816 942
817 - // Gather params to merge
818 - foreach ($box->params as $param_key => $param_value)
819 - {
820 - if (strpos($param_key, 'assign') === false || in_array($param_key, $params_to_ignore))
821 - {
822 - continue;
823 - }
943 + foreach ($boxes->posts as $box)
944 + {
945 + if ((int) $box->ID === $box_id)
946 + {
947 + $mirrored = $box;
948 + break;
949 + }
950 + }
824 951
825 - $assignments[$param_key] = $param_value;
952 + if (!$mirrored)
953 + {
954 + return $cache[$box_id];
826 955 }
827 -
828 - return new Registry($assignments);
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];
829 963 }
830 964
831 965 /**
832 966 * Prefixes the CSS classes
@@ -863,10 +997,15 @@
863 997 public function logOpenEvent($box_id, $page = null, $referrer = null)
864 998 {
865 999 $box = $this->get($box_id);
866 1000
1001 + if (!is_object($box) || !isset($box->params))
1002 + {
1003 + return;
1004 + }
1005 +
867 1006 // Do not track if statistics option is disabled
868 - $track_open_event = (bool) (is_null($box->params->get('stats', null)) ? BoxHelper::getParams()->get('stats', 1) : $box->params->get('stats'));
1007 + $track_open_event = (bool) (is_null($box->params->get('stats', null)) ? true : $box->params->get('stats'));
869 1008 if (!$track_open_event)
870 1009 {
871 1010 return;
872 1011 }
@@ -885,10 +1024,15 @@
885 1024 public function logCloseEvent($box_id, $box_log_id)
886 1025 {
887 1026 $box = $this->get($box_id);
888 1027
1028 + if (!is_object($box) || !isset($box->params))
1029 + {
1030 + return null;
1031 + }
1032 +
889 1033 // Do not track if statistics option is disabled
890 - $track_open_event = (bool) (is_null($box->params->get('stats', null)) ? BoxHelper::getParams()->get('stats', 1) : $box->params->get('stats'));
1034 + $track_open_event = (bool) (is_null($box->params->get('stats', null)) ? true : $box->params->get('stats'));
891 1035 if (!$track_open_event)
892 1036 {
893 1037 return null;
894 1038 }
@@ -894,69 +1038,85 @@
894 1038 }
895 1039
896 1040 firebox()->log->track($box_id, 2, $box_log_id);
897 1041 }
898 -
1042 +
899 1043 /**
900 - * Get box impressions
1044 + * Get total box impressions
901 1045 *
902 1046 * @param array $payload
903 1047 *
904 1048 * @return array
905 1049 */
906 - public function getImpressions($payload)
1050 + public function getTotalImpressions($payload)
907 1051 {
908 - return firebox()->tables->boxlog->getResults($payload);
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);
909 1055 }
910 -
1056 +
911 1057 /**
912 - * Get total box impressions
1058 + * Returns the cookie instance.
913 1059 *
914 - * @param array $payload
915 - *
916 - * @return array
1060 + * @return mixed
917 1061 */
918 - public function getTotalImpressions($payload)
1062 + public function getCookie()
919 1063 {
920 - return count($this->getImpressions($payload));
1064 + if (!$this->box)
1065 + {
1066 + return;
1067 + }
1068 +
1069 + return new Cookie($this->box);
921 1070 }
922 1071
923 1072 /**
924 - * Return the box settings
1073 + * Returns the box.
925 1074 *
926 1075 * @return object
927 1076 */
928 - public function getBoxSettings()
1077 + public function getBox()
929 1078 {
930 - return $this->boxSettings;
1079 + return $this->box;
931 1080 }
932 1081
933 1082 /**
934 - * Sets the box settings
1083 + * Sets the box.
935 1084 *
936 - * @param object $settings
1085 + * @param object $box
937 1086 *
938 - * @return object
1087 + * @return Box
939 1088 */
940 - public function setBoxSettings($settings)
1089 + public function setBox($box)
941 1090 {
942 - $this->boxSettings = $settings;
1091 + $this->box = $box;
1092 +
1093 + return $this;
943 1094 }
944 1095
945 - /**
946 - * Returns the box cookie
947 - *
948 - * @param string $cookie
949 - *
950 - * @return mixed
951 - */
952 - public function getCookie($cookie = null)
1096 + public function getParams()
953 1097 {
954 - $cookie = $cookie ? $cookie : (isset($this->boxSettings->ID) ? $this->boxSettings->ID : null);
955 - if (!isset($cookie))
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))
956 1116 {
957 - return;
1117 + $this->box->params = $params;
958 1118 }
959 -
960 - return new Cookie($cookie);
1119 +
1120 + return $this;
961 1121 }
962 -}
1122 +}