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

Firebox.php in FireBox – WooCommerce Popup Builder, Exit Intent Popup, Email Optin & Cart Abandonment trunk, at Inc/Core/Admin/Includes/Cpts/Firebox.php

500 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package FireBox
4 * @version 3.1.13 Free
5 *
6 * @author FirePlugins <info@fireplugins.com>
7 * @link https://www.fireplugins.com
8 * @copyright Copyright © 2026 FirePlugins All Rights Reserved
9 * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
10 */
11
12 namespace FireBox\Core\Admin\Includes\Cpts;
13
14 if (!defined('ABSPATH'))
15 {
16 exit; // Exit if accessed directly.
17 }
18
19 use FireBox\Core\Helpers\BoxHelper;
20
21 class Firebox
22 {
23 public $singular;
24 public $plural;
25
26 public function __construct()
27 {
28 $this->singular = 'FireBox';
29 $this->plural = firebox()->_('FB_FIREBOX_CAMPAIGNS');
30
31 register_post_type('firebox', $this->getPayload());
32
33 $this->init();
34 }
35
36 public function init()
37 {
38 add_action('admin_init', [$this, 'custom_post_edit_redirect']);
39
40 // set default post title
41 add_filter('default_title', [$this, 'set_default_campaign_title'], 10, 2);
42
43 // handle box duplication
44 add_action('admin_action_fb_duplicate_post_as_draft', [$this, 'handle_box_duplication']);
45
46 // handle box cookie clear
47 add_action('admin_action_fb_clear_cookie', [$this, 'handle_box_cookie_clear']);
48
49 // delete hook for FireBox
50 add_action('delete_post', [$this, 'before_delete_firebox_callback']);
51
52 add_action('load-firebox_page_firebox-campaigns', [$this, 'handle_bulk_actions']);
53
54 add_action('save_post', [$this, 'after_save']);
55 add_action('transition_post_status', [$this, 'track_first_campaign_creation_time'], 10, 3);
56
57 // Exclude FireBox from sitemaps
58 add_filter('wp_sitemaps_post_types', [$this, 'remove_post_type_from_wp_sitemap']);
59 add_filter('wpseo_sitemap_exclude_post_type', [$this, 'yoastseo_exclude_post_type'], 10, 2);
60
61 // Add noindex meta tag for single FireBox pages
62 add_filter('wp_robots', [$this, 'add_noindex_to_single_firebox']);
63 }
64
65 /**
66 * Exclude FireBox from Yoast SEO sitemaps
67 *
68 * @param bool $value
69 * @param string $post_type
70 *
71 * @return bool
72 */
73 public function yoastseo_exclude_post_type($value, $post_type)
74 {
75 $post_type_to_exclude = [
76 'firebox'
77 ];
78
79 if (in_array($post_type, $post_type_to_exclude))
80 {
81 $value = true;
82 }
83
84 return $value;
85 }
86
87 /**
88 * Remove the FireBox custom post type from the sitemap.
89 *
90 * This is not needed as published FireBox campaigns are not public.
91 *
92 * @param array $cpts
93 *
94 * @return array
95 */
96 public function remove_post_type_from_wp_sitemap($cpts)
97 {
98 if (isset($cpts['firebox']))
99 {
100 unset($cpts['firebox']);
101 }
102
103 return $cpts;
104 }
105
106 /**
107 * When going to edit.php?post_type=firebox, then
108 * redirect to: admin.php?page=firebox-campaigns
109 *
110 * @return void
111 */
112 function custom_post_edit_redirect()
113 {
114 global $pagenow;
115 if ($pagenow !== 'post-new.php' && isset($_GET['post_type']) && $_GET['post_type'] === 'firebox') //phpcs:ignore WordPress.Security.NonceVerification.Recommended
116 {
117 wp_redirect(admin_url('admin.php?page=firebox-campaigns'));
118 exit;
119 }
120 }
121
122 public function handle_bulk_actions()
123 {
124 if (empty($_GET))
125 {
126 return;
127 }
128
129 $action = isset($_GET['action']) ? sanitize_key($_GET['action']) : false;
130 if (!$action)
131 {
132 return;
133 }
134
135 if ($action !== 'fb_export')
136 {
137 return;
138 }
139
140 // Ensure we have IDs
141 $ids = isset($_GET['id']) ? array_map('intval', (array) wp_unslash($_GET['id'])) : [];
142 if (!$ids)
143 {
144 return;
145 }
146
147 // Get nonce
148 $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
149 if (!$nonce)
150 {
151 return;
152 }
153
154 // Verify nonce
155 $nonce_action = 'bulk-fireboxes';
156 if (!wp_verify_nonce($nonce, $nonce_action))
157 {
158 return;
159 }
160
161 BoxHelper::exportBoxes($ids);
162 \FPFramework\Libs\AdminNotice::displaySuccess(sprintf(firebox()->_('FB_X_CAMPAIGNS_HAVE_BEEN_EXPORTED'), count($ids)));
163 }
164
165 public function set_default_campaign_title($post_title, $post)
166 {
167 if (!isset($post->post_type))
168 {
169 return $post_title;
170 }
171
172 if ($post->post_type !== 'firebox')
173 {
174 return $post_title;
175 }
176
177 if (empty(trim($post_title)))
178 {
179 $post_title = firebox()->_('FB_UNTITLED_CAMPAIGN');
180 }
181
182 return $post_title;
183 }
184
185 /**
186 * Invalidates the framework cache after a campaign save.
187 *
188 * @param int $post_id
189 *
190 * @return void
191 */
192 public function after_save($post_id)
193 {
194 if (!isset($_POST['post_type']) || $_POST['post_type'] !== 'firebox')
195 {
196 return;
197 }
198
199 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
200 {
201 return;
202 }
203
204 if ($parent_id = wp_is_post_revision($post_id))
205 {
206 $post_id = $parent_id;
207 }
208
209 \FPFramework\Libs\Cache::invalidate();
210
211 // Invalidate the cached forms list (used by Form::getFormByID()) so it reflects this save.
212 \FireBox\Core\Helpers\Form\Form::clearFormsCache();
213 }
214
215 /**
216 * Tracks first campaign milestones once:
217 * - time to first draft
218 * - time to first publish
219 *
220 * @param string $new_status
221 * @param string $old_status
222 * @param \WP_Post $post
223 *
224 * @return void
225 */
226 public function track_first_campaign_creation_time($new_status, $old_status, $post)
227 {
228 if (!($post instanceof \WP_Post) || $post->post_type !== 'firebox')
229 {
230 return;
231 }
232
233 $start_time = $this->get_campaign_tracking_start_time();
234 if ($new_status === 'draft' && $old_status !== 'draft')
235 {
236 $draft_option = 'firebox_time_to_first_campaign_draft_seconds';
237 if (get_option($draft_option, '') === '')
238 {
239 update_option($draft_option, max(0, time() - $start_time), false);
240 }
241 }
242
243 if ($new_status === 'publish' && $old_status !== 'publish')
244 {
245 $publish_option = 'firebox_time_to_first_campaign_publish_seconds';
246 if (get_option($publish_option, '') === '')
247 {
248 update_option($publish_option, max(0, time() - $start_time), false);
249 }
250 }
251 }
252
253 /**
254 * Returns the start timestamp used for first campaign milestone tracking.
255 *
256 * @return int
257 */
258 private function get_campaign_tracking_start_time()
259 {
260 $option = 'firebox_first_campaign_tracking_started_at';
261 $started_at = (int) get_option($option, 0);
262 if ($started_at > 0)
263 {
264 return $started_at;
265 }
266
267 $started_at = time();
268 update_option($option, $started_at, false);
269 return $started_at;
270 }
271
272 /**
273 * Before deleting the FireBox, delete its box logs and box logs details data
274 *
275 * @param int $ID
276 *
277 * @return void
278 */
279 public function before_delete_firebox_callback($ID)
280 {
281 $post = get_post($ID);
282
283 if (!$post)
284 {
285 return;
286 }
287
288 if ($post->post_type !== 'firebox')
289 {
290 return;
291 }
292
293 $logs_table = firebox()->tables->boxlog->getFullTableName();
294 $logs_details_table = firebox()->tables->boxlogdetails->getFullTableName();
295
296 // Get all popup forms
297 $forms = \FPFramework\Helpers\Plugins\FireBox\Form::getCampaignForms($ID);
298
299 // delete logs details
300 firebox()->tables->boxlogdetails->executeRaw("DELETE FROM `$logs_details_table` WHERE log_id IN (SELECT id FROM `$logs_table` WHERE box = %d)", [$ID]);
301
302 // delete logs
303 firebox()->tables->boxlog->delete([
304 'box' => $ID
305 ]);
306
307 if ($forms)
308 {
309 foreach ($forms as $form_id => $form_label)
310 {
311 global $wpdb;
312
313 // Delete submission meta records first
314 $wpdb->query(
315 $wpdb->prepare(
316 "DELETE sm
317 FROM {$wpdb->prefix}firebox_submission_meta AS sm
318 INNER JOIN {$wpdb->prefix}firebox_submissions AS s ON sm.submission_id = s.id
319 WHERE s.form_id = %s"
320 ,
321 $form_id
322 )
323 );
324
325 // Delete submissions
326 $wpdb->delete("{$wpdb->prefix}firebox_submissions", ['form_id' => $form_id]);
327 }
328 }
329 }
330
331 /**
332 * Handles Box duplication
333 *
334 * @return void
335 */
336 public function handle_box_duplication()
337 {
338 check_admin_referer('duplicate-firebox-campaign');
339
340 $post_id = isset($_GET['post']) ? intval($_GET['post']) : '';
341
342 $redirect_url = admin_url() . 'admin.php?page=firebox-campaigns';
343
344 if (empty($post_id))
345 {
346 wp_redirect($redirect_url);
347 exit();
348 }
349
350 // The nonce is shared by the whole list; the per-post capability is what
351 // keeps a user from duplicating campaigns they cannot edit (same check the
352 // REST duplicate route performs).
353 if (!current_user_can('edit_post', $post_id))
354 {
355 wp_die(esc_html(fpframework()->_('FPF_CANNOT_VERIFY_REQUEST')));
356 }
357
358 BoxHelper::duplicateBox($post_id);
359
360 \FPFramework\Libs\AdminNotice::displaySuccess(firebox()->_('FB_CAMPAIGN_DUPLICATED'));
361
362 // redirect back to box list
363 wp_redirect($redirect_url);
364 }
365
366 /**
367 * Clears cookie from box
368 *
369 * @return void
370 */
371 public function handle_box_cookie_clear()
372 {
373 check_admin_referer('clearcookie-firebox-campaign');
374
375 $post_id = isset($_GET['post']) ? intval($_GET['post']) : '';
376
377 $redirect_url = admin_url() . 'admin.php?page=firebox-campaigns';
378
379 if (empty($post_id))
380 {
381 wp_redirect($redirect_url);
382 exit();
383 }
384
385 if (!current_user_can('edit_post', $post_id))
386 {
387 wp_die(esc_html__('Sorry, you are not allowed to do that.', 'firebox'));
388 }
389
390 if ($box = firebox()->box->get($post_id))
391 {
392 $cookie = new \FireBox\Core\FB\Cookie($box);
393 $cookie->remove();
394 }
395
396 // redirect back to box list
397 wp_redirect($redirect_url);
398 }
399
400 /**
401 * Adds noindex directive to single FireBox pages using WordPress robots filter.
402 * This integrates properly with WordPress core and other SEO plugins.
403 *
404 * @param array $robots Associative array of robots directives.
405 * @return array Modified robots directives.
406 */
407 public function add_noindex_to_single_firebox($robots)
408 {
409 if (is_singular('firebox'))
410 {
411 $robots['noindex'] = true;
412 $robots['nofollow'] = true;
413 }
414
415 return $robots;
416 }
417
418 /**
419 * Maps the post type's capabilities onto FireBox's own capability set.
420 *
421 * Kept public and static so the upgrade routine can reuse the same mapping when it
422 * works out which capabilities an existing role needs to keep its current access.
423 *
424 * @return array
425 */
426 public static function getPostTypeCapabilities()
427 {
428 return [
429 // Meta capabilities, resolved per post via map_meta_cap.
430 'edit_post' => 'edit_firebox',
431 'read_post' => 'read_firebox',
432 'delete_post' => 'delete_firebox',
433
434 // Primitive capabilities, assigned to roles.
435 'edit_posts' => 'edit_fireboxes',
436 'edit_others_posts' => 'edit_others_fireboxes',
437 'publish_posts' => 'publish_fireboxes',
438 'read_private_posts' => 'read_private_fireboxes',
439 'delete_posts' => 'delete_fireboxes',
440 'delete_private_posts' => 'delete_private_fireboxes',
441 'delete_published_posts' => 'delete_published_fireboxes',
442 'delete_others_posts' => 'delete_others_fireboxes',
443 'edit_private_posts' => 'edit_private_fireboxes',
444 'edit_published_posts' => 'edit_published_fireboxes',
445 'create_posts' => 'edit_fireboxes',
446 ];
447 }
448
449 /**
450 * Returns CPT payload
451 *
452 * @return array
453 */
454 protected function getPayload()
455 {
456 return [
457 'label' => firebox()->_('FB_PLUGIN_NAME'),
458 'public' => true,
459 'exclude_from_search' => true,
460 'show_ui' => true,
461 'show_in_menu' => false,
462 'has_archive' => false,
463 /**
464 * Campaigns are governed by FireBox's own capabilities, the same ones every
465 * AJAX handler and REST route checks. Registering against "post" meant the
466 * post type answered to core's edit_posts instead, so anyone who could write
467 * a blog post could write a campaign — and removing the FireBox capabilities
468 * from a role had no effect.
469 *
470 * map_meta_cap is what makes the per-post checks (current_user_can('edit_post',
471 * $id)) resolve through the mapping below.
472 */
473 'capability_type' => ['firebox', 'fireboxes'],
474 'map_meta_cap' => true,
475 'capabilities' => self::getPostTypeCapabilities(),
476 'hierarchical' => false,
477 'publicly_queryable' => true,
478 'rewrite' => ['slug' => 'firebox'],
479 'query_var' => true,
480 'show_in_rest' => true,
481 'supports' => ['title', 'editor', 'custom-fields'],
482 'labels' => [
483 'name' => firebox()->_('FB_PLUGIN_NAME'),
484 'singular_name' => $this->singular,
485 'add_new' => firebox()->_('FB_ADD_NEW_CAMPAIGN'),
486 'add_new_item' => firebox()->_('FB_ADD_NEW_CAMPAIGN'),
487 'edit_item' => firebox()->_('FB_EDIT_CAMPAIGN'),
488 'new_item' => firebox()->_('FB_NEW_CAMPAIGN'),
489 'all_items' => $this->plural,
490 'view_item' => firebox()->_('FB_VIEW_CAMPAIGN'),
491 'search_items' => firebox()->_('FB_SEARCH_CAMPAIGNS'),
492 'not_found' => firebox()->_('FB_NO_CAMPAIGNS_FOUND'),
493 'not_found_in_trash' => firebox()->_('FB_NO_CAMPAIGNS_FOUND_IN_TRASH'),
494 'parent_item_colon' => sprintf(firebox()->_('FB_PARENT_X'), $this->singular),
495 'menu_name' => firebox()->_('FB_PLUGIN_NAME'),
496 ]
497 ];
498 }
499 }
500