PluginProbe ʕ •ᴥ•ʔ
JetFormBuilder — Dynamic Blocks Form Builder / 3.5.6.3
JetFormBuilder — Dynamic Blocks Form Builder v3.5.6.3
3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.1.0 2.1.1 2.1.10 2.1.11 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 3.0.0 3.0.0.1 3.0.0.2 3.0.0.3 3.0.1 3.0.1.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.0.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.3.1 3.3.4 3.3.4.1 3.3.4.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.5.1 3.4.5.2 3.4.6 3.4.7 3.4.7.1 3.5.0 3.5.1 3.5.1.1 3.5.1.2 3.5.2 3.5.2.1 3.5.3 3.5.4 3.5.5 3.5.6 3.5.6.1 3.5.6.2 3.5.6.3 3.6.0
jetformbuilder / modules / user-journey / module.php
jetformbuilder / modules / user-journey Last commit date
admin 1 year ago models 1 year ago query-views 1 year ago rest-endpoints 1 year ago tab-handlers 1 year ago module.php 1 year ago tools.php 1 year ago user-journey-rest-controller.php 1 year ago
module.php
665 lines
1 <?php
2 namespace JFB_Modules\User_Journey;
3
4 use Jet_Form_Builder\Admin\Tabs_Handlers\Tab_Handler_Manager;
5 use JFB_Components\Module\Base_Module_After_Install_It;
6 use JFB_Components\Module\Base_Module_Dir_It;
7 use JFB_Components\Module\Base_Module_Dir_Trait;
8 use JFB_Components\Module\Base_Module_Handle_It;
9 use JFB_Components\Module\Base_Module_Handle_Trait;
10 use JFB_Components\Module\Base_Module_It;
11 use JFB_Components\Module\Base_Module_Static_Instance_It;
12 use JFB_Components\Module\Base_Module_Static_Instance_Trait;
13 use JFB_Components\Module\Base_Module_Url_It;
14 use JFB_Components\Module\Base_Module_Url_Trait;
15 use JFB_Modules\User_Journey\Admin\Meta_Boxes\Form_Record_User_Journey_Box;
16 use JFB_Modules\User_Journey\Models\User_Journey_Model;
17 use Jet_Form_Builder\Db_Queries\Exceptions\Sql_Exception;
18 use JFB_Modules\User_Journey\User_Journey_Rest_Controller;
19
20 // If this file is called directly, abort.
21 if ( ! defined( 'WPINC' ) ) {
22 die;
23 }
24
25 /**
26 * @method static Module instance()
27 *
28 * @package JFB_Modules\User_Journey
29 */
30 class Module implements
31 Base_Module_Dir_It,
32 Base_Module_It,
33 Base_Module_Handle_It,
34 Base_Module_Url_It,
35 Base_Module_After_Install_It,
36 Base_Module_Static_Instance_It {
37
38 use Base_Module_Handle_Trait;
39 use Base_Module_Dir_Trait;
40 use Base_Module_Url_Trait;
41 use Base_Module_Static_Instance_Trait;
42
43 private $rest;
44 private $forms;
45 /**
46 * Returns the instance ID for the module.
47 *
48 * @return string The instance ID.
49 */
50 public static function get_instance_id(): string {
51 return 'user-journey';
52 }
53
54 /**
55 * Checks if the form-record module is available.
56 *
57 * @return bool True if the module is available, false otherwise.
58 */
59 public function condition(): bool {
60 return jet_form_builder()->has_module( 'form-record' );
61 }
62
63 /**
64 * Initializes hooks for the module.
65 */
66 public function init_hooks() {
67 add_action(
68 'rest_api_init',
69 array( $this->get_rest(), 'register_routes' )
70 );
71
72 add_filter(
73 'jet-form-builder/page-containers/jfb-records-single',
74 array( $this, 'add_box_to_single_record' ),
75 0
76 );
77
78 if ( $this->is_user_journey_enabled() ) {
79
80 add_action(
81 'jet-form-builder/form-handler/before-send',
82 array( $this, 'add_query_args' ),
83 10
84 );
85
86 add_filter(
87 'jet-form-builder/actions/redirect-to-page/redirect-args',
88 array( $this, 'add_redirect_to_page_action_query_args' ),
89 10,
90 2
91 );
92
93 add_filter(
94 'jet-fb/response-handler/query-args',
95 array( $this, 'add_gateway_query_args' ),
96 10,
97 2
98 );
99
100 add_action(
101 'wp_footer',
102 array( $this, 'enqueue_journey_script' )
103 );
104
105 add_filter(
106 'jet-form-builder/editor/config',
107 array( $this, 'add_user_journey_settings' ),
108 10,
109 1
110 );
111
112 add_action(
113 'jet-form-builder/form-record/save-record-action',
114 array( $this, 'save_user_journey' ),
115 10,
116 2
117 );
118
119 add_action(
120 'wp_head',
121 array( $this, 'clear_journey_data_on_reload' ),
122 -9999,
123 2
124 );
125 }
126 }
127
128 /**
129 * Removes hooks for the module.
130 */
131 public function remove_hooks() {
132 remove_action(
133 'rest_api_init',
134 array( $this->get_rest(), 'register_routes' )
135 );
136
137 remove_filter(
138 'jet-form-builder/page-containers/jfb-records-single',
139 array( $this, 'add_box_to_single_record' ),
140 0
141 );
142
143 if ( $this->is_user_journey_enabled() ) {
144 remove_action(
145 'wp_footer',
146 array( $this, 'enqueue_journey_script' )
147 );
148
149 remove_filter(
150 'jet-form-builder/editor/config',
151 array( $this, 'add_user_journey_settings' )
152 );
153
154 remove_action(
155 'jet-form-builder/form-record/save-record-action',
156 array( $this, 'save_user_journey' )
157 );
158
159 remove_action(
160 'wp_head',
161 array( $this, 'clear_journey_data_on_reload' )
162 );
163
164 remove_filter(
165 'jet-form-builder/actions/redirect-to-page/redirect-args',
166 array( $this, 'add_redirect_to_page_action_query_args' )
167 );
168
169 remove_action(
170 'jet-form-builder/form-handler/before-send',
171 array( $this, 'add_query_args' )
172 );
173 }
174 }
175
176 /**
177 * Handles actions to perform on module installation.
178 */
179 public function on_install() {
180 $this->rest = new User_Journey_Rest_Controller();
181 $this->set_forms();
182
183 Tab_Handler_Manager::instance()->install( new Tab_Handlers\User_Journey_Handler() );
184 }
185
186 /**
187 * Handles actions to perform on module uninstallation.
188 */
189 public function on_uninstall() {
190 Tab_Handler_Manager::instance()->uninstall( new Tab_Handlers\User_Journey_Handler() );
191 }
192
193 public function set_forms() {
194 if ( ! \JFB_Modules\Post_Type\Module::class ) {
195 return array();
196 }
197
198 $this->forms = get_posts(
199 array(
200 'post_type' => \JFB_Modules\Post_Type\Module::SLUG,
201 'posts_per_page' => -1,
202 )
203 );
204 }
205
206 public function get_forms() {
207 return $this->forms;
208 }
209
210 public function add_gateway_query_args( $args, $response ) {
211 if ( ! \JFB_Modules\Post_Type\Module::class ) {
212 return $args;
213 }
214
215 if ( isset( $response->args['form_id'] ) && isset( $response->args['status'] ) ) {
216 if ( false === strpos( $args['status'], '%7C' ) && false === strpos( $args['status'], '|' ) ) {
217 return $args;
218 } else if ( false !== strpos( $args['status'], '%7C' ) ) {
219 $status = explode( '%7C', $args['status'] )[0];
220 } else {
221 $status = explode( '|', $args['status'] )[0];
222 }
223
224 $jet_fb_user_journey_settings = $this->get_journey_settings();
225
226 if ( 'success' === $jet_fb_user_journey_settings['clear_after_submit'] ) {
227 if ( 'dsuccess' === $status ) {
228 $args['jfb_clear_journey'] = $response->args['form_id'];
229 }
230 } else if ( 'always' === $jet_fb_user_journey_settings['clear_after_submit'] ) {
231 $args['jfb_clear_journey'] = $response->args['form_id'];
232 }
233 }
234
235 return $args;
236 }
237
238 public function add_redirect_to_page_action_query_args( $args, $handler ) {
239 if ( ! \JFB_Modules\Post_Type\Module::class ) {
240 return $args;
241 }
242
243 $post_type_module = \Jet_Form_Builder\Plugin::instance()->module( \JFB_Modules\Post_Type\Module::class );
244 $journey_form_ids = $this->get_form_ids_with_save_user_journey();
245
246 $forms = $this->get_forms();
247
248 foreach ( $forms as $form ) {
249 $actions = $post_type_module->get_actions( $form->ID );
250 foreach ( $actions as $action ) {
251 if ( ( $action['type'] ?? '' ) === 'redirect_to_page' ) {
252 if ( isset( $handler->_id ) ) {
253 if ( in_array( $form->ID, $journey_form_ids ) && $action['id'] === $handler->_id ) {
254 $args['jfb_clear_journey'] = $form->ID;
255 }
256 }
257 break;
258 }
259 }
260 }
261
262 return $args;
263 }
264
265 public function get_form_ids_with_save_user_journey() {
266 $post_type_module = \Jet_Form_Builder\Plugin::instance()->module( \JFB_Modules\Post_Type\Module::class );
267 $matched_form_ids = array();
268
269 $forms = $this->get_forms();
270
271 foreach ( $forms as $form ) {
272 $actions = $post_type_module->get_actions( $form->ID );
273
274 foreach ( $actions as $action ) {
275 if ( ( $action['type'] ?? '' ) === 'save_record' ) {
276 $save_user_journey = $action['settings']['save_record']['save_user_journey'] ?? false;
277
278 if ( $save_user_journey && true === $save_user_journey ) {
279 $matched_form_ids[] = $form->ID;
280 }
281 }
282 }
283 }
284
285 return $matched_form_ids;
286 }
287
288 public function add_query_args( $instance ) {
289 $form_id = $instance->form_id;
290
291 $instance->add_response_data( array( 'jfb_clear_journey' => $form_id ) );
292 }
293
294 /**
295 * Adds user journey settings to the provided settings array.
296 *
297 * @param array $settings The settings array to modify.
298 *
299 * @return array The modified settings array.
300 */
301 public function add_user_journey_settings( $settings ) {
302 $user_journey_settings = Tab_Handler_Manager::get_options( 'user-journey-tab' );
303
304 $settings['user_journey'] = array(
305 'enabled' => $user_journey_settings['enable_user_journey'] ?? false,
306 'storage_type' => $user_journey_settings['storage_type'] ?? 'local',
307 'clear_after_submit' => $user_journey_settings['clear_after_submit'] ?? 'success',
308 );
309
310 return $settings;
311 }
312
313 /**
314 * Adds a meta box to a single record.
315 *
316 * @param Base_Meta_Container[] $containers The array of meta containers.
317 *
318 * @return array The modified array of meta containers.
319 */
320 public function add_box_to_single_record( array $containers ): array {
321 $containers[0]->add_meta_box( new Form_Record_User_Journey_Box() );
322
323 return $containers;
324 }
325
326 /**
327 * Checks if user journey tracking is enabled in the settings.
328 *
329 * @return bool True if user journey tracking is enabled, false otherwise.
330 */
331 public function is_user_journey_enabled() {
332 $options = Tab_Handler_Manager::get_options( 'user-journey-tab' );
333 return ! empty( $options['enable_user_journey'] );
334 }
335
336 /**
337 * Returns the journey tracking settings.
338 *
339 * @return array The journey settings.
340 */
341 public function get_journey_settings() {
342 $options = Tab_Handler_Manager::get_options( 'user-journey-tab' );
343 return array(
344 'enabled' => ! empty( $options['enable_user_journey'] ),
345 'storage_type' => $options['storage_type'] ?? 'local',
346 'clear_after_submit' => $options['clear_after_submit'] ?? 'success',
347 'form_ids_with_save_user_journey' => $this->get_form_ids_with_save_user_journey(),
348 );
349 }
350
351 public function clear_journey_data_on_reload() {
352 $jet_fb_user_journey_settings = $this->get_journey_settings();
353 ?>
354 <script>
355 ( function() {
356 const settings = <?php echo wp_json_encode( $jet_fb_user_journey_settings ); ?>;
357 const storageKey = 'jet_fb_user_journey';
358 const storage = settings.storage_type === 'local' ? localStorage : sessionStorage;
359
360 var params = new URLSearchParams( window.location.search );
361
362 if ( params.has('jfb_clear_journey') ) {
363 var status = params.get('status') ?? 'unknown',
364 formId = params.get('jfb_clear_journey');
365
366 if ( status.includes('|') ) {
367 status = status.split('|')[0];
368 }
369
370 params.delete('jfb_clear_journey');
371
372 var newSearch = params.toString();
373
374 var newUrl = window.location.origin
375 + window.location.pathname
376 + ( newSearch ? '?' + newSearch : '' )
377 + window.location.hash;
378
379 window.history.replaceState( null, document.title, newUrl );
380
381 if ( ( 'success' === settings.clear_after_submit && ( 'success' === status || 'dsuccess' === status ) )
382 || 'always' === settings.clear_after_submit || 'unknown' === status )
383 {
384 const raw = storage.getItem( storageKey );
385
386 if ( ! raw ) {
387 return;
388 }
389
390 let data = JSON.parse( raw );
391
392 if ( ! Array.isArray( data ) && data.hasOwnProperty( formId ) ) {
393 delete data[ formId ];
394 }
395
396 storage.setItem( storageKey, JSON.stringify( data ) );
397 }
398 }
399 } )();
400 </script>
401 <?php
402 }
403
404 /**
405 * Enqueues JavaScript for tracking the user journey.
406 */
407 public function enqueue_journey_script() {
408 $jet_fb_user_journey_settings = $this->get_journey_settings();
409 ?>
410 <script>
411 ( function() {
412 const settings = <?php echo wp_json_encode( $jet_fb_user_journey_settings ); ?>;
413 const storageKey = 'jet_fb_user_journey';
414 const storage = settings.storage_type === 'local' ? localStorage : sessionStorage;
415 const currentUrl = window.location.pathname;
416 const queryString = decodeURIComponent( window.location.search );
417 const formIds = new Set( ( settings.form_ids_with_save_user_journey || [] ).map( Number ) );
418
419 let journeys;
420
421 try {
422 journeys = JSON.parse( storage.getItem( storageKey ) ) || {};
423 } catch {
424 journeys = {};
425 }
426
427 Object.keys( journeys ).forEach( id => {
428 if ( ! formIds.has( +id ) ) delete journeys[ id ];
429 } );
430
431 const pageEntry = { url: currentUrl, query: queryString, timestamp: Date.now() };
432
433 function addJourney( journeys, formIds, to_form_id = null ) {
434
435 if ( null !== to_form_id ) {
436 journeys[ to_form_id ] = [];
437 journeys[ to_form_id ].push( pageEntry );
438 } else {
439 formIds.forEach( formId => {
440 if ( ! journeys[ formId ] ) {
441 journeys[ formId ] = [];
442 }
443
444 const journey = journeys[ formId ];
445 const lastEntry = 0 < journey.length ? journey[ journey.length - 1 ] : false;
446
447 if ( ! lastEntry ) {
448 journey.push( pageEntry );
449 } else if ( currentUrl !== lastEntry.url || queryString !== lastEntry.query ) {
450 journey.push( pageEntry );
451 }
452 } );
453 }
454
455 storage.setItem( storageKey, JSON.stringify( journeys ) );
456 }
457
458 function clearJourney( form, on_success = true ) {
459 const formElement = form instanceof jQuery ? form[0] : form;
460 const formId = formElement.dataset.formId;
461
462 let can_clear = false;
463
464 if ( on_success ) {
465 if ( 'success' === settings.clear_after_submit || 'always' === settings.clear_after_submit ) {
466 can_clear = true;
467 }
468 } else {
469 if ( 'always' === settings.clear_after_submit ) {
470 can_clear = true;
471 }
472 }
473
474 if ( formElement ) {
475 if ( formIds.has( +formId ) ) {
476
477 if ( can_clear ) {
478 const raw = storage.getItem( storageKey );
479
480 if ( ! raw ) {
481 return;
482 }
483
484 let data = JSON.parse( raw );
485
486 if ( ! Array.isArray( data ) && data.hasOwnProperty( formId ) ) {
487 delete data[ formId ];
488 }
489
490 storage.setItem( storageKey, JSON.stringify( data ) );
491
492 const savedJourneys = storage.getItem( storageKey );
493
494 if ( savedJourneys ) {
495 journeys = JSON.parse( savedJourneys );
496 }
497
498 addJourney( journeys, formIds, formId );
499
500 if ( window?.JetFormBuilderSettings?.devmode ) {
501 /* eslint-disable no-console */
502 console.group( 'User Journeys' );
503 console.info( journeys );
504 console.groupEnd();
505 /* eslint-enable no-console */
506 }
507 }
508 }
509 } else {
510 console.error('Form element is not defined or not a valid DOM element.');
511 }
512 }
513
514 addJourney( journeys, formIds );
515
516 jQuery( document ).ready( function() {
517 if ( window?.JetFormBuilderSettings?.devmode ) {
518 /* eslint-disable no-console */
519 console.group( 'User Journeys' );
520 console.info( journeys );
521 console.groupEnd();
522 /* eslint-enable no-console */
523 }
524
525 JetPlugins.hooks.addFilter(
526 'jet.fb.submit.ajax.promises',
527 'user_journey_promise',
528 function ( promises, $form ) {
529 promises.push( new Promise( ( resolve, reject ) => {
530 const formElement = $form instanceof jQuery ? $form[0] : $form;
531 const formId = formElement.dataset.formId;
532 const userJourneyData = storage.getItem( storageKey );
533 const userJourneyDataParsed = JSON.parse(userJourneyData || '{}');
534 const formSpecificData = userJourneyDataParsed[formId] || '';
535
536 if ( formElement ) {
537 if ( formIds.has( +formId ) ) {
538 if ( userJourneyData ) {
539 let hiddenInput = $form[0].querySelector( 'input[name="_user_journey"]' );
540
541 if ( !hiddenInput ) {
542 hiddenInput = document.createElement( 'input' );
543
544 hiddenInput.type = 'hidden';
545 hiddenInput.name = '_user_journey';
546
547 $form[0].appendChild( hiddenInput );
548 }
549 hiddenInput.value = JSON.stringify( formSpecificData );
550 }
551 }
552 }
553
554 resolve();
555 } ) );
556
557 return promises;
558 }
559 );
560
561 JetPlugins.hooks.addFilter(
562 'jet.fb.submit.reload.promises',
563 'user_journey_promise',
564 function( $promises, $context ) {
565 $promises.push(
566 new Promise( ( resolve ) => {
567 const rootNode = $context.target;
568 const userJourneyData = storage.getItem( storageKey );
569 const formId = rootNode.dataset.formId;
570 const userJourneyDataParsed = JSON.parse(userJourneyData || '{}');
571 const formSpecificData = userJourneyDataParsed[formId] || '';
572
573 if ( formIds.has( +formId ) && userJourneyData ) {
574 let hiddenInput = rootNode.querySelector( 'input[name="_user_journey"]' );
575
576 if ( ! hiddenInput ) {
577 hiddenInput = document.createElement( 'input' );
578
579 hiddenInput.type = 'hidden';
580 hiddenInput.name = '_user_journey';
581
582 rootNode.appendChild( hiddenInput );
583 }
584 hiddenInput.value = JSON.stringify( formSpecificData );
585 }
586 resolve();
587 } )
588 );
589
590 return $promises;
591 }
592 );
593
594 jQuery( document ).on('jet-form-builder/ajax/on-success', function( event, response, form ) {
595 clearJourney( form );
596 } );
597
598 jQuery( document ).on('jet-form-builder/ajax/processing-error', function( event, response, form ) {
599 clearJourney( form, false );
600 } );
601
602 jQuery( document ).on('jet-form-builder/ajax/on-fail', function( event, jqXHR, textStatus, errorThrown, form ) {
603 clearJourney( form, false );
604 } );
605 } );
606
607 } )();
608 </script>
609 <?php
610 }
611
612 /**
613 * Saves the user journey to the database after form submission.
614 *
615 * @param int $record_id The ID of the form record.
616 * @param array $action_request The action request data.
617 */
618 public function save_user_journey( $record_id, $action_request ) {
619 try {
620 ( new User_Journey_Model() )->create();
621 } catch ( Sql_Exception $e ) {
622 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
623 error_log( $e->getMessage() );
624 }
625 }
626
627 if ( ! $record_id ) {
628 return;
629 }
630
631 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
632 $user_journey = isset( $_POST['_user_journey'] ) ? sanitize_text_field( $_POST['_user_journey'] ) : null;
633
634 if ( ! $user_journey ) {
635 return;
636 }
637
638 $journey_data = json_decode( wp_unslash( $user_journey ), true );
639
640 if ( ! is_array( $journey_data ) ) {
641 return;
642 }
643
644 foreach ( $journey_data as $step => $item ) {
645 $journey_results[] = array(
646 'record_id' => $record_id,
647 'journey_step' => $step,
648 'journey_url' => sanitize_text_field( $item['url'] ),
649 'journey_query' => urldecode( $item['query'] ?? '' ),
650 'timestamp' => $item['timestamp'],
651 );
652 }
653
654 ( new User_Journey_Model() )->insert_many( $journey_results );
655 }
656
657 /**
658 * Returns the REST controller for the user journey.
659 *
660 * @return User_Journey_Rest_Controller The REST controller instance.
661 */
662 public function get_rest(): User_Journey_Rest_Controller {
663 return $this->rest;
664 }
665 }