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/Helpers/BoxHelper.php +514 -85 1.0.12 → 3.1.14 View file →
@@ -1,12 +1,12 @@
1 1 <?php
2 2 /**
3 3 * @package FireBox
4 - * @version 1.0.12 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\Helpers;
@@ -20,27 +20,405 @@
20 20
21 21 class BoxHelper
22 22 {
23 23 /**
24 + * Option holding the cached campaign lists, keyed by status/limit.
25 + *
26 + * An autoloaded option rather than a transient on purpose: a transient with an expiry is
27 + * stored non-autoloaded, so reading it costs 2 queries per request on stock WordPress —
28 + * exactly what the campaign query it replaces costs. Autoloaded means WordPress has
29 + * already loaded it by the time Boxes::render() runs. There is no TTL either: the option
30 + * is deleted on every campaign write, so the cache is always current.
31 + *
32 + * @var string
33 + */
34 + const BOXES_CACHE_OPTION = 'firebox_boxes_cache';
35 +
36 + /**
37 + * Returns the prefix every FireBox cookie name is built from.
38 + *
39 + * Cookies are set with path=/, so on a subdirectory network every site of
40 + * example.com/store, example.com/blog … shares one cookie namespace. Post
41 + * IDs restart per site, so campaign 42 exists on both and one campaign's
42 + * "already seen" cookie would suppress an unrelated campaign on another
43 + * site. The blog ID in the name is what keeps them apart.
44 + *
45 + * Single sites — and the main site of a network, which owns the unprefixed
46 + * namespace already — keep the original prefix, so upgrading does not show
47 + * a campaign again to visitors who had dismissed it.
48 + *
49 + * @return string
50 + */
51 + public static function cookiePrefix()
52 + {
53 + if (!is_multisite() || is_main_site())
54 + {
55 + return 'firebox_';
56 + }
57 +
58 + return 'firebox_' . get_current_blog_id() . '_';
59 + }
60 +
61 + /**
62 + * The path FireBox cookies are written to.
63 + *
64 + * WordPress derives COOKIEPATH from the site's home URL, so it already
65 + * points at exactly the part of the domain this site occupies — the whole
66 + * domain for a normal install, /store/ for that site of a subdirectory
67 + * network. It is what WooCommerce and AffiliateWP scope their cookies with,
68 + * and it keeps a network's sites from writing over each other.
69 + *
70 + * @return string
71 + */
72 + public static function cookiePath()
73 + {
74 + return \FPFramework\Helpers\CookieScope::path();
75 + }
76 +
77 + /**
78 + * The domain FireBox cookies are written to.
79 + *
80 + * Empty unless the site defines COOKIE_DOMAIN, which is how a site tells
81 + * WordPress its cookies belong to something other than the current host —
82 + * shared between www and non-www, or across the subdomains of a network.
83 + * Ignoring it means writing a cookie the site cannot read back.
84 + *
85 + * @return string
86 + */
87 + public static function cookieDomain()
88 + {
89 + return \FPFramework\Helpers\CookieScope::domain();
90 + }
91 +
92 + /**
93 + * Paths a cookie of ours may have been written to before.
94 + *
95 + * Cookies are keyed by name *and* path, so a cookie written at '/' is a
96 + * different cookie from the same name at '/store/'. FireBox wrote every
97 + * cookie to '/' until it started honouring COOKIEPATH, and expiring only
98 + * the new path would leave the old one behind — still sent, still
99 + * suppressing the campaign it belongs to.
100 + *
101 + * @return array
102 + */
103 + public static function cookiePathsToClear()
104 + {
105 + return \FPFramework\Helpers\CookieScope::pathsToClear();
106 + }
107 +
108 + /**
109 + * Get box meta
110 + *
111 + * @param int $id
112 + *
113 + * @return array
114 + */
115 + public static function getMeta($id)
116 + {
117 + $meta = get_post_meta($id, 'firebox_meta', false);
118 + $meta = isset($meta[0]) && is_array($meta[0]) ? $meta[0] : [];
119 +
120 + return $meta;
121 + }
122 +
123 + /**
124 + * Attaches inline JavaScript that runs once the FireBox runtime has created all
125 + * campaign instances.
126 + *
127 + * The script is attached in the "before" position on purpose: WordPress downgrades a
128 + * defer-strategy script to a blocking one as soon as it carries an "after" inline
129 + * script, so a single campaign with custom code would make the whole runtime
130 + * parse-blocking. Printing before the runtime keeps defer intact.
131 + *
132 + * Waiting for DOMContentLoaded is what makes that safe: firebox-main is deferred, so
133 + * the browser executes it once parsing is done — at which point FireBox.onReady()
134 + * runs immediately (readyState is already "interactive") and builds every campaign
135 + * instance. DOMContentLoaded fires after all deferred scripts, so by the time this
136 + * code runs the instances exist.
137 + *
138 + * @param string $js
139 + *
140 + * @return void
141 + */
142 + public static function addInlineScript($js)
143 + {
144 + if (!is_string($js) || trim($js) === '')
145 + {
146 + return;
147 + }
148 +
149 + $wrapped = 'document.addEventListener("DOMContentLoaded", function(){' . $js . '});';
150 +
151 + wp_add_inline_script('firebox-main', $wrapped, 'before');
152 + }
153 +
154 + /**
155 + * Whether any of the given campaigns uses session-dependent behavior:
156 + * the Pageviews/Time on Site display conditions, or an impressions
157 + * limit scoped to the visitor's session (fpfsid).
158 + *
159 + * These are stamped and evaluated against JS-set session cookies, so
160 + * when in use the frontend runtime must be enqueued on every page.
161 + * Skipped entirely when no campaign needs them, so other sites load no
162 + * extra JS or cookies.
163 + *
164 + * @param \WP_Query $boxes
165 + *
166 + * @return bool
167 + */
168 + public static function sessionConditionsInUse($boxes)
169 + {
170 + // The answer only changes when a campaign is written to, so keep it in an
171 + // autoloaded option instead of re-scanning every campaign's meta per page view.
172 + $cached = get_option('firebox_session_conditions_in_use');
173 +
174 + if ($cached === 'yes' || $cached === 'no')
175 + {
176 + return $cached === 'yes';
177 + }
178 +
179 + $inUse = self::computeSessionConditionsInUse($boxes);
180 +
181 + update_option('firebox_session_conditions_in_use', $inUse ? 'yes' : 'no', true);
182 +
183 + return $inUse;
184 + }
185 +
186 + /**
187 + * Drops the cached session-conditions flag so it is recomputed on the next
188 + * frontend request. Hooked on every write path that can change the answer.
189 + *
190 + * @return void
191 + */
192 + public static function invalidateSessionConditionsFlag()
193 + {
194 + delete_option('firebox_session_conditions_in_use');
195 + }
196 +
197 + /**
198 + * Drops every campaign-derived cache: the session-conditions flag and the
199 + * cached box list. Hooked on each write path that can change either answer.
200 + *
201 + * @return void
202 + */
203 + public static function invalidateCaches()
204 + {
205 + self::invalidateSessionConditionsFlag();
206 + self::invalidateBoxesCache();
207 + }
208 +
209 + /**
210 + * Registers the invalidation hooks for the campaign-derived caches.
211 + *
212 + * @return void
213 + */
214 + public static function registerCacheInvalidation()
215 + {
216 + add_action('save_post_firebox', [self::class, 'invalidateCaches']);
217 +
218 + // These fire for every post type, so only react to campaigns — otherwise
219 + // ordinary content edits would keep busting the campaign caches.
220 + $onPostChange = function($post_id, $post = null)
221 + {
222 + if (self::isCampaign($post_id, $post))
223 + {
224 + self::invalidateCaches();
225 + }
226 + };
227 +
228 + add_action('deleted_post', $onPostChange, 10, 2);
229 + add_action('trashed_post', $onPostChange, 10, 1);
230 + add_action('untrashed_post', $onPostChange, 10, 1);
231 +
232 + // firebox_meta is also written outside the save_post flow (Gutenberg REST meta
233 + // updates, the campaign importer, duplication).
234 + $onMetaWrite = function($meta_id, $post_id, $meta_key)
235 + {
236 + if ($meta_key === 'firebox_meta')
237 + {
238 + self::invalidateCaches();
239 + }
240 + };
241 +
242 + add_action('added_postmeta', $onMetaWrite, 10, 3);
243 + add_action('updated_postmeta', $onMetaWrite, 10, 3);
244 + }
245 +
246 + /**
247 + * Whether the given post is a campaign. `deleted_post` passes the post object
248 + * along because the row is already gone by the time it fires.
249 + *
250 + * @param int $post_id
251 + * @param \WP_Post $post
252 + *
253 + * @return bool
254 + */
255 + private static function isCampaign($post_id, $post = null)
256 + {
257 + if ($post instanceof \WP_Post)
258 + {
259 + return $post->post_type === 'firebox';
260 + }
261 +
262 + return get_post_type($post_id) === 'firebox';
263 + }
264 +
265 + /**
266 + * Scans the given campaigns for session-dependent behavior.
267 + *
268 + * @param \WP_Query $boxes
269 + *
270 + * @return bool
271 + */
272 + private static function computeSessionConditionsInUse($boxes)
273 + {
274 + if (!$boxes || empty($boxes->posts))
275 + {
276 + return false;
277 + }
278 +
279 + foreach ($boxes->posts as $box)
280 + {
281 + $meta = self::getMeta($box->ID);
282 +
283 + if (!$meta)
284 + {
285 + continue;
286 + }
287 +
288 + // Impressions limit scoped to the browser session
289 + $impressionsPeriod = isset($meta['assign_impressions_param_type']) ? $meta['assign_impressions_param_type'] : '';
290 +
291 + if ($impressionsPeriod === 'custom')
292 + {
293 + $impressionsPeriod = isset($meta['assign_impressions_param_custom_period']) ? $meta['assign_impressions_param_custom_period'] : '';
294 + }
295 +
296 + if ($impressionsPeriod === 'session')
297 + {
298 + return true;
299 + }
300 +
301 + // Session-dependent display conditions
302 + $metaJson = wp_json_encode($meta);
303 +
304 + if (strpos($metaJson, 'Pageviews') !== false || strpos($metaJson, 'TimeOnSite') !== false)
305 + {
306 + return true;
307 + }
308 + }
309 +
310 + return false;
311 + }
312 +
313 + /**
24 314 * Gets all Boxes.
25 - * We use get_posts instead of firebox()->tables->box->getResults
26 - * as we need it on Widgets initialization where we don't have the framework
27 - * available yet to query the database with our wrapper
28 - *
29 - * @return array
315 + *
316 + * Only the campaign IDs are cached, in an autoloaded option so the list also
317 + * survives across requests on hosts without a persistent object cache — where
318 + * wp_cache is per-request and the query therefore ran on every page view. The
319 + * autoloaded options are already in memory by the time this runs, so a cache hit
320 + * costs no query of its own.
321 + *
322 + * There is no TTL: the cache is dropped on every campaign write, which also means
323 + * editors see their changes immediately.
324 + *
325 + * @param array $status
326 + * @param int $limit
327 + *
328 + * @return \WP_Query
30 329 */
31 - public static function getAllBoxes($status = 'publish')
330 + public static function getAllBoxes($status = ['publish'], $limit = -1)
32 331 {
33 - $payload = [
332 + $key = self::getBoxesCacheKey($status, $limit);
333 + $cache = get_option(self::BOXES_CACHE_OPTION, []);
334 + $cache = is_array($cache) ? $cache : [];
335 +
336 + if (isset($cache[$key]) && is_array($cache[$key]))
337 + {
338 + return self::hydrateBoxesQuery($cache[$key]);
339 + }
340 +
341 + $args = [
34 342 'post_status' => $status,
35 343 'post_type' => 'firebox',
36 - 'numberposts' => -1
344 + 'posts_per_page' => $limit,
345 + 'no_found_rows' => true,
346 + 'update_post_term_cache' => false,
347 + 'cache_results' => true
37 348 ];
38 349
39 - return get_posts($payload);
350 + // Get the query.
351 + $query = new \WP_Query($args);
352 +
353 + wp_reset_postdata();
354 +
355 + $cache[$key] = wp_list_pluck($query->posts, 'ID');
356 +
357 + update_option(self::BOXES_CACHE_OPTION, $cache, true);
358 +
359 + return $query;
40 360 }
41 361
42 362 /**
363 + * Rebuilds a WP_Query from a cached list of campaign IDs.
364 + *
365 + * @param array $ids
366 + *
367 + * @return \WP_Query
368 + */
369 + private static function hydrateBoxesQuery($ids)
370 + {
371 + $query = new \WP_Query();
372 +
373 + // A hand-built query has no query vars of its own, and the loop reads several
374 + // while setting up each post. "fields => all" states that $posts already holds
375 + // full post objects, which is what it takes for the loop to use them as-is.
376 + $query->query_vars = $query->fill_query_vars(['fields' => 'all']);
377 +
378 + $posts = [];
379 +
380 + if ($ids)
381 + {
382 + // Two queries, both by primary key, priming the same post and postmeta
383 + // caches the original WP_Query did.
384 + _prime_post_caches($ids, false, true);
385 +
386 + // array_filter drops campaigns deleted since the cache was written.
387 + $posts = array_values(array_filter(array_map('get_post', $ids)));
388 + }
389 +
390 + $query->posts = $posts;
391 + $query->post_count = count($posts);
392 + $query->found_posts = $query->post_count;
393 +
394 + return $query;
395 + }
396 +
397 + /**
398 + * The cache entry a box list is stored under.
399 + *
400 + * @param array $status
401 + * @param int $limit
402 + *
403 + * @return string
404 + */
405 + private static function getBoxesCacheKey($status, $limit)
406 + {
407 + return md5(implode(',', (array) $status) . '_' . (int) $limit);
408 + }
409 +
410 + /**
411 + * Drops every cached box list.
412 + *
413 + * @return void
414 + */
415 + public static function invalidateBoxesCache()
416 + {
417 + delete_option(self::BOXES_CACHE_OPTION);
418 + }
419 +
420 + /**
43 421 * Retrieves all boxes in a key => value array of ID => title
44 422 *
45 423 * @return array
46 424 */
@@ -50,9 +428,9 @@
50 428 {
51 429 return [];
52 430 }
53 431
54 - return self::produceKeyValueBoxes($boxes);
432 + return self::produceKeyValueBoxes($boxes->posts);
55 433 }
56 434
57 435 /**
58 436 * Produce a key,value pair of boxes containg their ID,title
@@ -92,9 +470,9 @@
92 470 }
93 471
94 472 $boxes = firebox()->tables->box->getResults([
95 473 'where' => [
96 - 'ID' => ' NOT IN (' . esc_sql($id) . ')',
474 + 'ID' => ' NOT IN (' . absint($id) . ')',
97 475 'post_status' => " = 'publish'",
98 476 'post_type' => " = 'firebox'"
99 477 ]
100 478 ]);
@@ -126,9 +504,9 @@
126 504 $box = (int) $box;
127 505
128 506 $box = firebox()->tables->box->getResults([
129 507 'where' => [
130 - 'ID' => " = '" . esc_sql($box) . "'"
508 + 'ID' => " = '" . absint($box) . "'"
131 509 ]
132 510 ]);
133 511
134 512 return isset($box[0]) ? $box[0] : [];
@@ -151,9 +529,9 @@
151 529 $box = (int) $box;
152 530
153 531 $box = firebox()->tables->box->getResults([
154 532 'where' => [
155 - 'ID' => " = '" . esc_sql($box) . "'"
533 + 'ID' => " = '" . absint($box) . "'"
156 534 ]
157 535 ]);
158 536
159 537 if (!$box)
@@ -204,9 +582,9 @@
204 582 // cache key
205 583 $cache_key = md5('fboxSettings');
206 584
207 585 // check cache
208 - if ($params = wp_cache_get($cache_key))
586 + if ($params = wp_cache_get($cache_key, 'firebox'))
209 587 {
210 588 return $params;
211 589 }
212 590
@@ -213,57 +591,14 @@
213 591 // get params
214 592 $params = get_option('firebox_settings');
215 593
216 594 // set cache
217 - wp_cache_set($cache_key, $params);
595 + wp_cache_set($cache_key, $params, 'firebox', 5 * MINUTE_IN_SECONDS);
218 596
219 597 return $params;
220 598 }
221 -
222 - /**
223 - * Finds and returns the box from $_GET['template']
224 - *
225 - * @return mixed
226 - */
227 - public static function getBoxFromTemplate()
228 - {
229 - $template = isset($_GET['template']) ? sanitize_text_field($_GET['template']) : '';
230 -
231 - if (empty($template))
232 - {
233 - return '';
234 - }
235 -
236 - return firebox()->library->find($template);
237 - }
238 599
239 600 /**
240 - * Transforms an array of key,value to inline CSS
241 - *
242 - * @param array $array
243 - *
244 - * @return string
245 - */
246 - public static function arrayToCSSS($array)
247 - {
248 - $array = array_filter($array);
249 -
250 - if (empty($array))
251 - {
252 - return '';
253 - }
254 -
255 - $styles = '';
256 -
257 - foreach ($array as $key => $value)
258 - {
259 - $styles .= $key . ':' . $value . ';';
260 - }
261 -
262 - return $styles;
263 - }
264 -
265 - /**
266 601 * Duplicates a box
267 602 *
268 603 * @param integer $box_id
269 604 *
@@ -273,10 +608,10 @@
273 608 {
274 609 // get box
275 610 $box = firebox()->tables->box->getResults([
276 611 'where' => [
277 - 'ID' => " = '" . esc_sql($box_id) . "'",
278 - 'post_status' => " = '" . esc_sql(get_post_status($box_id)) . "'",
612 + 'ID' => " = '" . absint($box_id) . "'",
613 + 'post_status' => " = '" . sanitize_key(get_post_status($box_id)) . "'",
279 614 'post_type' => " = 'firebox'"
280 615 ],
281 616 'limit' => 1
282 617 ]);
@@ -290,17 +625,42 @@
290 625 $box = $box[0];
291 626 $box->ID = '';
292 627 $box->post_title = 'Copy of ' . $box->post_title;
293 628 $box->post_status = 'draft';
294 -
629 +
630 + /**
631 + * The copy belongs to whoever duplicates, never the source author — the same
632 + * rule the importer applies. The PHP and custom-JavaScript gates are a
633 + * function of the post author, so keeping the source author would let a copy
634 + * of a higher-privileged user's campaign keep running code its new owner is
635 + * not allowed to write.
636 + */
637 + $box->post_author = get_current_user_id();
638 +
639 + $factory = new \FPFramework\Base\Factory();
640 +
641 + $tz = wp_timezone();
642 + $date_without_tz = $factory->getDate();
643 + $date_with_tz = $factory->getDate()->setTimezone($tz);
644 +
645 + $box->post_date = $date_with_tz->format('Y-m-d H:i:s');
646 + $box->post_date_gmt = $date_without_tz->format('Y-m-d H:i:s');
647 +
648 + Form\Form::ensureUniqueFormIDs($box->post_content);
649 +
650 + // get meta options
651 + $meta = self::getMeta($box_id);
652 +
295 653 // insert new box
296 654 $new_box_id = firebox()->tables->box->insert($box);
297 655
298 - // get meta options
299 - $meta = get_post_meta($box_id, 'fpframework_meta_settings', true);
656 + // add meta options for new box
657 + // TODO: In the future, use "firebox_meta". This is a temporary fix for backwards compatibility.
658 + $checkMeta = (array) $meta;
659 + $meta_key = isset($checkMeta['width']) ? 'firebox_meta' : 'fpframework_meta_settings';
660 + update_post_meta($new_box_id, $meta_key, wp_slash($meta));
300 661
301 - // add meta options for new box
302 - update_post_meta($new_box_id, 'fpframework_meta_settings', $meta);
662 + return $new_box_id;
303 663 }
304 664
305 665 /**
306 666 * Reset Box Stats
@@ -310,8 +670,16 @@
310 670 * @return void
311 671 */
312 672 public static function resetBoxStats($box_ids)
313 673 {
674 + // These ids are concatenated into raw IN() clauses below, so cast them here rather
675 + // than trusting every caller to do it.
676 + $box_ids = array_filter(array_map('intval', (array) $box_ids));
677 + if (empty($box_ids))
678 + {
679 + return;
680 + }
681 +
314 682 $logs_table = firebox()->tables->boxlog->getFullTableName();
315 683 $logs_details_table = firebox()->tables->boxlogdetails->getFullTableName();
316 684
317 685 // delete box logs details
@@ -321,20 +689,22 @@
321 689 firebox()->tables->boxlog->deleteRaw('WHERE box IN (' . implode(',', $box_ids) . ')');
322 690 }
323 691
324 692 /**
325 - * Exports boxes
326 - *
693 + * Builds the export payload (boxes + meta) and a suggested filename for the given box ids,
694 + * without emitting any headers or output. Shared by exportBoxes() (legacy file-download
695 + * action) and the Campaigns REST export endpoint.
696 + *
327 697 * @param array $box_ids
328 - *
329 - * @return string
698 + *
699 + * @return array|null ['filename' => string, 'exported' => array] or null if no boxes matched
330 700 */
331 - public static function exportBoxes($box_ids)
701 + public static function getExportPayload($box_ids)
332 702 {
333 703 // get boxes
334 704 $boxes = firebox()->tables->box->getResults([
335 705 'where' => [
336 - 'ID' => ' IN (' . implode(',', esc_sql($box_ids)) . ')',
706 + 'ID' => ' IN (' . implode(',', array_map('intval', $box_ids)) . ')',
337 707 'post_type' => " = 'firebox'"
338 708 ]
339 709 ]);
340 710
@@ -341,13 +711,13 @@
341 711 $boxes = (array) $boxes;
342 712
343 713 if (!count($boxes))
344 714 {
345 - return;
715 + return null;
346 716 }
347 717
348 718 $exported = [];
349 -
719 +
350 720 $filename = firebox()->_('FB_PLUGIN_NAME') . ' Items';
351 721
352 722 // name for 1 box
353 723 if (count($boxes) == 1)
@@ -352,10 +722,12 @@
352 722 // name for 1 box
353 723 if (count($boxes) == 1)
354 724 {
355 725 $name = mb_strtolower(html_entity_decode($boxes['0']->post_title));
356 - $name = preg_replace('#[^a-z0-9_-]#', '_', $name);
357 - $name = trim(preg_replace('#__+#', '_', $name), '_-');
726 + // preg_replace() returns null when PCRE fails (backtrack limit, invalid UTF-8),
727 + // and passing null on is deprecated since PHP 8.1. Keep the previous value.
728 + $name = preg_replace('#[^a-z0-9_-]#', '_', $name) ?? $name;
729 + $name = trim(preg_replace('#__+#', '_', $name) ?? $name, '_-');
358 730
359 731 $filename = firebox()->_('FB_PLUGIN_NAME') . ' Item (' . $name . ')';
360 732 }
361 733
@@ -360,9 +732,9 @@
360 732 }
361 733
362 734 foreach ($boxes as $box)
363 735 {
364 - $meta = get_post_meta($box->ID, 'fpframework_meta_settings', true);
736 + $meta = self::getMeta($box->ID);
365 737
366 738 $exported[] = [
367 739 'box' => $box,
368 740 'meta' => $meta
@@ -368,16 +740,38 @@
368 740 'meta' => $meta
369 741 ];
370 742 }
371 743
372 - $string = json_encode($exported);
373 -
744 + return [
745 + 'filename' => $filename,
746 + 'exported' => $exported,
747 + ];
748 + }
749 +
750 + /**
751 + * Exports boxes
752 + *
753 + * @param array $box_ids
754 + *
755 + * @return string
756 + */
757 + public static function exportBoxes($box_ids)
758 + {
759 + if (!$payload = self::getExportPayload($box_ids))
760 + {
761 + return;
762 + }
763 +
764 + $filename = $payload['filename'];
765 + $exported = $payload['exported'];
766 +
374 767 // SET DOCUMENT HEADER
375 - if (preg_match('#Opera(/| )([0-9].[0-9]{1,2})#', $_SERVER['HTTP_USER_AGENT']))
768 + $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
769 + if (preg_match('#Opera(/| )([0-9].[0-9]{1,2})#', $userAgent))
376 770 {
377 771 $UserBrowser = "Opera";
378 772 }
379 - elseif (preg_match('#MSIE ([0-9].[0-9]{1,2})#', $_SERVER['HTTP_USER_AGENT']))
773 + elseif (preg_match('#MSIE ([0-9].[0-9]{1,2})#', $userAgent))
380 774 {
381 775 $UserBrowser = "IE";
382 776 }
383 777 else
@@ -403,8 +797,43 @@
403 797 header('Pragma: no-cache');
404 798 }
405 799
406 800 // PRINT STRING
407 - echo $string;
408 - die;
801 + echo wp_json_encode($exported);
802 +
803 + if (ob_get_level())
804 + {
805 + @ob_end_flush();
806 + }
807 + die();
409 808 }
410 -}
809 +
810 + /**
811 + * Returns the last date viewed of a campaign.
812 + *
813 + * @param int $id
814 + *
815 + * @return string
816 + */
817 + public static function getCampaignLastDateViewed($id = null)
818 + {
819 + if (!$id)
820 + {
821 + return;
822 + }
823 +
824 + $last_date_viewed = firebox()->tables->boxlog->getResults([
825 + 'select' => [
826 + 'date as last_date_viewed'
827 + ],
828 + 'where' => [
829 + 'box' => ' = ' . absint($id),
830 + ],
831 + 'orderby' => ' date desc',
832 + 'limit' => 1
833 + ]);
834 +
835 + return isset($last_date_viewed[0]->last_date_viewed) ? get_date_from_gmt($last_date_viewed[0]->last_date_viewed) : null;
836 + }
837 +
838 +
839 +}