LLMS_Forms

LLMS_Forms class


Source Source

File: includes/forms/class-llms-forms.php

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
class LLMS_Forms {
 
    use LLMS_Trait_Singleton;
 
    /**
     * Minimum Supported WP Version required to manage forms with the block editor UI.
     */
    const MIN_WP_VERSION = '5.7.0';
 
    /**
     * Provide access to the post type manager class
     *
     * @var LLMS_Forms_Post_Type
     */
    public $post_type_manager = null;
 
    /**
     * Private Constructor
     *
     * @since 5.0.0
     *
     * @return void
     */
    private function __construct() {
 
        $this->post_type_manager = new LLMS_Form_Post_Type( $this );
 
        add_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 );
        add_filter( 'llms_get_form_post', array( $this, 'maybe_load_preview' ) );
 
    }
 
    /**
     * Determines if the WP core requirements are met
     *
     * This is used to determine if the block editor can be used to manage forms and fields,
     * all frontend and server-side handling works on all core supported WP versions.
     *
     * @since 5.0.0
     *
     * @return boolean
     */
    public function are_requirements_met() {
        global $wp_version;
        return version_compare( $wp_version, self::MIN_WP_VERSION, '>=' ) || is_plugin_active( 'gutenberg/gutenberg.php' );
    }
 
    /**
     * Determine if usernames are enabled on the site.
     *
     * This method is used to determine if a username can be used to login / reset a user's password.
     *
     * A reference to every form with a username block is stored in an option. The option is an array
     * of integers, the WP_Post IDs of all the form posts containing a username block.
     *
     * If the array is empty, there are no forms with username blocks and, therefore, usernames are disabled.
     * If the array contains at least one item that means there is a form with a username block in it and,
     * we therefore consider usernames to be enabled for the site.
     *
     * This isn't perfect. We're well aware. But usernames are kind of silly anyway, right? Just use the email
     * address like your average website owner and stop pretending usernames matter.
     *
     * @since 5.0.0
     *
     * @return bool
     */
    public function are_usernames_enabled() {
 
        $locations = get_option( 'llms_forms_username_locations', array() );
 
        /**
         * Use this to explicitly enable of disable username fields.
         *
         * Note that usage of this filter will not actually disable the llms/form-field-username block.
         * It's possible to create a confusing user experience by explicitly disabling usernames and
         * leaving username field blocks on one or more forms. If you decide to explicitly disable via
         * this filter you should also remove all the username blocks from all of your forms.
         *
         * @since 5.0.0
         *
         * @param boolean $enabled Whether or not usernames are enabled.
         */
        return apply_filters( 'llms_are_usernames_enabled', ! empty( $locations ) );
 
    }
 
    /**
     * Converts a block to settings understandable by `llms_form_field()`
     *
     * @since 5.0.0
     * @since 5.1.0 Added logic to remove invisible fields.
     *              Added `$block_list` param.
     *
     * @param array   $block      A WP Block array.
     * @param array[] $block_list Optional. The list of WP Block array `$block` comes from. Default is empty array.
     * @return array
     */
    private function block_to_field_settings( $block, $block_list = array() ) {
 
        $is_visible = $this->is_block_visible_in_list( $block, $block_list );
 
        /**
         * Filters whether or not invisible fields should be included
         *
         * If the block is not visible (according to LLMS block-level visibility settings)
         * it will return an empty array (signaling the field to be removed).
         *
         * @since 5.1.0
         *
         * @param boolean $filter     Whether or not invisible fields should be included. Default is `false`.
         * @param array   $block      A WP Block array.
         * @param array[] $block_list The list of WP Block array `$block` comes from.
         */
        if ( ! $is_visible && apply_filters( 'llms_forms_remove_invisible_field', false, $block, $block_list ) ) {
            return array();
        }
 
        $attrs = $this->convert_settings_format( $block['attrs'], 'block' );
 
        // If the field is required and hidden it's impossible for the user to fill it out so it gets marked as optional at runtime.
        if ( ! empty( $attrs['required'] ) && ! $is_visible ) {
            $attrs['required'] = false;
        }
 
        /**
         * Filter an LLMS_Form_Field settings array after conversion from a field block
         *
         * @since 5.0.0
         * @since 5.1.0 Added `$block_list` param.
         *
         * @param array   $attrs      An array of LLMS_Form_Field settings.
         * @param array   $block      A WP Block array.
         * @param array[] $block_list The list of WP Block array `$block` comes from.
         */
        return apply_filters( 'llms_forms_block_to_field_settings', $attrs, $block, $block_list );
 
    }
 
    /**
     * Cascade all llms_visibility attributes down into inner blocks.
     *
     * If a parent block has a visibility setting this will apply that visibility to a chlid block *if*
     * the child block does not have a visibility setting of its own.
     *
     * Ultimately this ensures that a field block that's not visible can be marked as "optional" so that
     * form validation can take place.
     *
     * For example, if a columns block is displayed only to logged out users and it's child fields are marked
     * as required that means that it's required only to logged out users and the field becomes "optional"
     * (for validation purposes) to logged in users.
     *
     * @since 5.0.0
     *
     * @param array[]     $blocks     Array of parsed block arrays.
     * @param string|null $visibility The llms_visibility attribute of the parent block which is applied to all innerBlocks
     *                                if the innerBlock does not already have it's own visibility attribute.
     * @return array[]
     */
    private function cascade_visibility_attrs( $blocks, $visibility = null ) {
 
        foreach ( $blocks as &$block ) {
 
            // If a visibility setting has been passed from the parent and the block does not have visibility setting of it's own.
            if ( $visibility && ( empty( $block['attrs']['llms_visibility'] ) || 'off' === $block['attrs']['llms_visibility'] ) ) {
                $block['attrs']['llms_visibility'] = $visibility;
            }
 
            // This block has a visibility attribute and it should be applied it to all the innerBlocks.
            if ( ! empty( $block['attrs']['llms_visibility'] ) && ! empty( $block['innerBlocks'] ) ) {
                $block['innerBlocks'] = $this->cascade_visibility_attrs( $block['innerBlocks'], $block['attrs']['llms_visibility'] );
            }
        }
 
        return $blocks;
 
    }
 
    /**
     * Converts field settings formats
     *
     * There are small differences between the LLMS_Form_Fields settings array
     * and the WP_Block settings array.
     *
     * This method accepts an associative array
     * in one format or the other and converts it from the original format to the opposite format.
     *
     * @since 5.0.0
     *
     * @param array  $map            Associative array of settings.
     * @param string $orignal_format The original format of the submitted `$map`. Either "field" for
     *                               an array of LLMS_Form_Field settings or `block` for an array
     *                               of WP_Block attributes.
     * @return [type] [description]
     */
    private function convert_settings_format( $map, $orignal_format ) {
 
        // Block attributes to LLMS_Form_Field settings.
        $keys = array(
            'field'      => 'type',
            'className'  => 'classes',
            'html_attrs' => 'attributes',
        );
 
        // LLMS_Form_Field settings to block attributes.
        if ( 'field' === $orignal_format ) {
            $keys = array_flip( $keys );
        }
 
        // Loop through the original map and rename the necessary keys.
        foreach ( $keys as $orig_key => $new_key ) {
            if ( isset( $map[ $orig_key ] ) ) {
                $map[ $new_key ] = $map[ $orig_key ];
                unset( $map[ $orig_key ] );
            }
        }
 
        return $map;
 
    }
 
    /**
     * Converts an array of LLMS_Form_Field settings to a block attributes array
     *
     * @since 5.0.0
     *
     * @param array $settings An array of LLMS_Form_Field settings.
     * @return array An array of WP_Block attributes.
     */
    public function convert_settings_to_block_attrs( $settings ) {
        return $this->convert_settings_format( $settings, 'field' );
    }
 
    /**
     * Create a form for a given location with the provided data.
     *
     * @since 5.0.0
     *
     * @param string $location_id Location id.
     * @param bool   $recreate    If `true` and the form already exists, will recreate the existing form using the existing form's id.
     * @return int|false Returns the created/update form post ID on success.
     *                   If the location doesn't exist, returns `false`.
     *                   If the form already exists and `$recreate` is `false` will return `false`.
     */
    public function create( $location_id, $recreate = false ) {
 
        if ( ! $this->is_location_valid( $location_id ) ) {
            return false;
        }
 
        $locs = $this->get_locations();
        $data = $locs[ $location_id ];
 
        $existing = $this->get_form_post( $location_id );
 
        // Form already exists and we haven't requested an update.
        if ( false !== $existing && ! $recreate ) {
            return false;
        }
 
        $args = array(
            'ID'           => $existing ? $existing->ID : 0,
            'post_content' => LLMS_Form_Templates::get_template( $location_id ),
            'post_status'  => 'publish',
            'post_title'   => $data['title'],
            'post_type'    => $this->get_post_type(),
            'meta_input'   => $data['meta'],
            'post_author'  => $existing ? $existing->post_author : LLMS_Install::get_can_install_user_id(),
        );
 
        /**
         * Filter arguments used to install a new form.
         *
         * @since 5.0.0
         *
         * @param array  $args        Array of arguments to be passed to wp_insert_post
         * @param string $location_id Location ID/name.
         * @param array  $data        Array of location information from LLMS_Forms::get_locations().
         */
        $args = apply_filters( 'llms_forms_install_post_args', $args, $location_id, $data );
 
        return wp_insert_post( $args );
 
    }
 
    /**
     * Retrieve the form management user capability.
     *
     * @since 5.0.0
     *
     * @return string
     */
    public function get_capability() {
        return $this->post_type_manager->capability;
    }
 
    /**
     * Pull LifterLMS Form Field blocks from an array of parsed WP Blocks.
     *
     * Searches innerBlocks arrays recursively.
     *
     * @since 5.0.0
     * @since 5.1.0 First check block's innerBlock attribute exists when checking for inner blocks.
     *              Also made the access visibility public.
     * @since 5.9.0 Pass an empty string to `strpos()` instead of `null`.
     *
     * @param array $blocks Array of WP Block arrays from `parse_blocks()`.
     * @return array
     */
    public function get_field_blocks( $blocks ) {
 
        $fields = array();
 
        foreach ( $blocks as $block ) {
 
            if ( ! empty( $block['innerBlocks'] ) ) {
                $fields = array_merge( $fields, $this->get_field_blocks( $block['innerBlocks'] ) );
            } elseif ( false !== strpos( $block['blockName'] ?? '', 'llms/form-field-' ) ) {
                $fields[] = $block;
            } elseif ( 'core/html' === $block['blockName'] && ! empty( $block['attrs']['type'] ) ) {
                $fields[] = $block;
            }
        }
 
        return $fields;
 
    }
 
    /**
     * Returns a list of field names used by LifterLMS forms
     *
     * Used to validate uniqueness of custom field data.
     *
     * @since 5.0.0
     *
     * @return string[]
     */
    public function get_field_names() {
 
        $names = array(
            'user_login',
            'user_login_confirm',
            'email_address',
            'email_address_confirm',
            'password',
            'password_confirm',
            'first_name',
            'last_name',
            'display_name',
            'llms_billing_address_1',
            'llms_billing_address_2',
            'llms_billing_city',
            'llms_billing_country',
            'llms_billing_state',
            'llms_billing_zip',
            'llms_phone',
        );
 
        /**
         * Filters the list of field names used by LifterLMS forms
         *
         * @since 5.0.0
         *
         * @param string[] $names List of registered field names.
         */
        return apply_filters( 'llms_forms_field_names', $names );
 
    }
 
    /**
     * Retrieve an array of parsed blocks for the form at a given location.
     *
     * @since 5.0.0
     *
     * @param string $location Form location, one of: "checkout", "registration", or "account".
     * @param array  $args     Additional arguments passed to the short-circuit filter.
     * @return array|false
     */
    public function get_form_blocks( $location, $args = array() ) {
 
        $post = $this->get_form_post( $location, $args );
        if ( ! $post ) {
            return false;
        }
 
        $content  = $post->post_content;
        $content .= $this->get_additional_fields_html( $location, $args );
 
        $blocks = $this->parse_blocks( $content );
 
        /**
         * Filters the parsed block list for a given LifterLMS form
         *
         * This hook can be used to programmatically modify, insert, or remove
         * blocks (fields) from a form.
         *
         * @since 5.0.0
         *
         * @param array[] $blocks   Array of parsed WP_Block arrays.
         * @param string  $location The request form location ID.
         * @param array   $args     Additional arguments passed to the short-circuit filter.
         */
        return apply_filters( 'llms_get_form_blocks', $blocks, $location, $args );
 
    }
 
    /**
     * Retrieve an array of LLMS_Form_Fields settings arrays for the form at a given location.
     *
     * This method is used by the LLMS_Form_Handler to perform validations on user-submitted data.
     *
     * @since 5.0.0
     *
     * @param string $location Form location, one of: "checkout", "registration", or "account".
     * @param array  $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.
     * @return false|array
     */
    public function get_form_fields( $location, $args = array() ) {
 
        $blocks = $this->get_form_blocks( $location, $args );
 
        if ( false === $blocks ) {
            return false;
        }
 
        $fields = $this->get_fields_settings_from_blocks( $blocks );
 
        /**
         * Modify the parsed array of LifterLMS Form Fields
         *
         * @since 5.0.0
         *
         * @param array[] $fields   Array of LifterLMS Form Field settings data.
         * @param string  $location Form location, one of: "checkout", "registration", or "account".
         * @param array   $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.
         */
        return apply_filters( 'llms_get_form_fields', $fields, $location, $args );
 
    }
 
    /**
     * Retrieve an array of LLMS_Form_Field settings from an array of blocks.
     *
     * @since 5.0.0
     * @since 5.1.0 Pass the whole list of blocks to the `$this->block_to_field_settings()` method
     *              to better check whether a block is visible.
     * @since 6.2.0 Exploded hidden checkbox fields.
     *
     * @param array $blocks Array of WP Block arrays from `parse_blocks()`.
     * @return array
     */
    public function get_fields_settings_from_blocks( $blocks ) {
 
        $fields = array();
        $blocks = $this->get_field_blocks( $blocks );
 
        foreach ( $blocks as $block ) {
            $settings = $this->block_to_field_settings( $block, $blocks );
 
            if ( empty( $settings ) ) {
                continue;
            }
            if (
                'hidden' === ( $settings['type'] ?? null ) &&
                isset( $block['attrs']['field'] ) && 'checkbox' === $block['attrs']['field']
            ) {
                // Convert hidden checkbox settings into multiple "checked" hidden fields.
                $settings['type'] = $block['attrs']['field'];
                $field            = new LLMS_Form_Field( $settings );
                $form_fields      = $field->explode_options_to_fields( true );
                foreach ( $form_fields as $form_field ) {
                    $fields[] = $form_field->get_settings();
                }
            } else {
                $field    = new LLMS_Form_Field( $settings );
                $fields[] = $field->get_settings();
            }
        }
 
        return $fields;
    }
 
    /**
     * Retrieve a field item from a list of fields by a key/value pair.
     *
     * @since 5.0.0
     *
     * @param array[] $fields List of LifterLMS Form Fields.
     * @param string  $key    Setting key to search for.
     * @param mixed   $val    Setting valued to search for.
     * @param string  $return Determine the return value. Use "field" to return the field settings
     *                        array. Use "index" to return the index of the field in the $fields array.
     * @return array|int|false `false` when the field isn't found in $fields, otherwise returns the field settings
     *                          as an array when `$return` is "field". Otherwise returns the field's index as an int.
     */
    public function get_field_by( $fields, $key, $val, $return = 'field' ) {
 
        foreach ( $fields as $index => $field ) {
            if ( isset( $field[ $key ] ) && $val === $field[ $key ] ) {
                return 'field' === $return ? $field : $index;
            }
        }
 
        return false;
 
    }
 
    /**
     * Retrieve the rendered HTML for the form at a given location.
     *
     * @since 5.0.0
     *
     * @param string $location Form location, one of: "checkout", "registration", or "account".
     * @param array  $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.
     * @return string
     */
    public function get_form_html( $location, $args = array() ) {
 
        $blocks = $this->get_form_blocks( $location, $args );
        if ( ! $blocks ) {
            return '';
        }
 
        $disable_visibility = ( 'checkout' !== $location );
 
        // Force fields to display regardless of visibility settings when viewing account/registration forms.
        if ( $disable_visibility ) {
            add_filter( 'llms_blocks_visibility_should_filter_block', '__return_false', 999 );
        }
 
        $html = '';
        foreach ( $blocks as $block ) {
            $html .= render_block( $block );
        }
 
        if ( $disable_visibility ) {
            remove_filter( 'llms_blocks_visibility_should_filter_block', '__return_false', 999 );
        }
 
        /**
         * Modify the parsed array of LifterLMS Form Fields.
         *
         * @since 5.0.0
         *
         * @param string $html     Form fields HTML.
         * @param string $location Form location, one of: "checkout", "registration", or "account".
         * @param array  $args     Additional arguments passed to the short-circuit filter in `get_form_post()`.
         */
        return apply_filters( 'llms_get_form_html', $html, $location, $args );
 
    }
 
    /**
     * Retrieve the WP Post for the form at a given location.
     *
     * @since 5.0.0
     *
     * @param string $location Form location, one of: "checkout", "registration", or "account".
     * @param array  $args     Additional arguments passed to the short-circuit filter.
     * @return WP_Post|false
     */
    public function get_form_post( $location, $args = array() ) {
 
        // @todo Add caching. This runs twice on some page loads.
 
        /**
         * Skip core lookup of the form for the request location and return a custom form post.
         *
         * @since 5.0.0
         *
         * @param null|WP_Post $post     Return a WP_Post object to short-circuit default lookup query.
         * @param string       $location Form location. Either "checkout", "registration", or "account".
         * @param array        $args     Additional custom arguments.
         */
        $post = apply_filters( 'llms_get_form_post_pre_query', null, $location, $args );
        if ( is_a( $post, 'WP_Post' ) ) {
            return $post;
        }
 
        $query = new WP_Query(
            array(
                'post_type'      => $this->get_post_type(),
                'posts_per_page' => 1,
                'no_found_rows'  => true,
                // Only show published forms to end users but allow admins to "preview" drafts.
                'post_status'    => current_user_can( $this->get_capability() ) ? array( 'publish', 'draft' ) : 'publish',
                'meta_query'     => array(
                    'relation' => 'AND',
                    array(
                        'key'   => '_llms_form_location',
                        'value' => $location,
                    ),
                    array(
                        'key'   => '_llms_form_is_core',
                        'value' => 'yes',
                    ),
                ),
            )
        );
 
        $post = $query->have_posts() ? $query->posts[0] : false;
 
        /**
         * Filters the returned `llms_form` post object
         *
         * @since 5.0.0
         *
         * @param WP_Post|boolean $post     The post object of the form or `false` if no form could be located.
         * @param string       $location Form location. Either "checkout", "registration", or "account".
         * @param array        $args     Additional custom arguments.
         */
        return apply_filters( 'llms_get_form_post', $post, $location, $args );
 
    }
 
    /**
     * Check whether a given form is a core form.
     *
     * When there are multiple forms for a location, the core form is identified as the one with the lowest ID.
     *
     * @since 6.4.0
     *
     * @param WP_Post|int $form Form's WP_Post instance, or its ID.
     * @return boolean
     */
    public function is_a_core_form( $form ) {
 
        $form_id = $form instanceof WP_Post ? $form->ID : $form;
 
        if ( ! $form_id ) {
            return false;
        }
 
        return in_array( $form_id, $this->get_core_forms( 'ids' ), true );
 
    }
 
    /**
     * Retrieves only core forms.
     *
     * When there are multiple forms for a location, the core form is identified as the one with the lowest ID.
     *
     * @since 6.4.0
     *
     * @param string $return What to return: 'posts', for an array of WP_Post; 'ids' for an array of WP_Post ids.
     * @return WP_Post[]|int[]
     */
    private function get_core_forms( $return = 'posts', $use_cache = true ) {
 
        global $wpdb;
 
        $forms_cache_key = 'posts' === $return ? 'llms_core_forms' : 'llms_core_form_ids';
        $forms           = $use_cache ? wp_cache_get( $forms_cache_key ) : false;
 
        if ( false !== $forms ) {
            return $forms;
        }
 
        $locations              = array_keys( $this->get_locations() );
        $locations_placeholders = implode( ',', array_fill( 0, count( $locations ), '%s' ) );
        $prepare_values         = array_merge( array( $this->get_post_type() ), $locations );
 
        $query = "
SELECT MIN({$wpdb->posts}.ID) AS ID
FROM $wpdb->posts
INNER JOIN {$wpdb->postmeta} AS locations ON {$wpdb->posts}.ID = locations.post_id AND locations.meta_key='_llms_form_location'
INNER JOIN {$wpdb->postmeta} AS is_cores ON {$wpdb->posts}.ID = is_cores.post_id AND is_cores.meta_key='_llms_form_is_core'
WHERE {$wpdb->posts}.post_type = %s
AND locations.meta_value IN ({$locations_placeholders})
AND is_cores.meta_value = 'yes'
GROUP BY locations.meta_value";
 
        $form_ids = $wpdb->get_col(
            $wpdb->prepare(
                $query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is prepared.
                $prepare_values
            )
        );
 
        $form_ids = array_map( 'absint', $form_ids );
        $forms    = 'post' === $return ? array_map( 'get_post', $form_ids ) : $form_ids;
 
        wp_cache_set( $forms_cache_key, $forms );
 
        return $forms;
 
    }
 
 
    /**
     * Retrieve additional fields added to the form programmatically.
     *
     * @since 5.0.0
     *
     * @param string $location Form location, one of: "checkout", "registration", or "account".
     * @param array  $args     Additional arguments passed to the short-circuit filter.
     * @return array[]
     */
    private function get_additional_fields( $location, $args = array() ) {
 
        /**
         * Filter to add custom fields to a form programmatically.
         *
         * @since 3.0.0
         * @since 5.0.0 Moved from deprecated function `LLMS_Person_Handler::get_available_fields()`.
         *
         * @param array[] $fields   Array of field array suitable to pass to `llms_form_field()`.
         * @param string  $location Form location, one of: "checkout", "registration", or "account".
         * @param array   $args     Additional arguments passed to the short-circuit filter.
         */
        return apply_filters( 'lifterlms_get_person_fields', array(), $location, $args );
 
    }
 
    /**
     * Retrieve HTML for the form's additional programmatically-added fields.
     *
     * Gets the HTML for each field from `llms_form_field()` and wraps it as a `wp/html` block.
     *
     * @since 5.0.0
     *
     * @param string $location Form location, one of: "checkout", "registration", or "account".
     * @param array  $args     Additional arguments passed to the short-circuit filter.
     * @return string
     */
    private function get_additional_fields_html( $location, $args = array() ) {
 
        $html   = '';
        $fields = $this->get_additional_fields( $location, $args );
 
        foreach ( $fields as $field ) {
            $html .= "\r" . $this->get_custom_field_block_markup( $field );
        }
 
        return $html;
 
    }
 
    /**
     * Retrieve the HTML markup for a custom form field block
     *
     * Retrieves an array of `LLMS_Form_Field` settings, generates the HTML
     * for the field, and wraps it in a `wp:html` block.
     *
     * @since 5.0.0
     *
     * @param array $settings Form field settings (passed to `llms_form_field()`).
     * @return string
     */
    public function get_custom_field_block_markup( $settings ) {
        return sprintf( '<!-- wp:html %1$s -->%2$s%3$s%2$s<!-- /wp:html -->', wp_json_encode( $settings ), "\r", llms_form_field( $settings, false ) );
    }
 
    /**
     * Retrieve an array of form fields used for the "free enrollment" form
     *
     * This is the "one-click" enrollment form used when a logged-in user clicks the "checkout" button
     * from an access plan.
     *
     * This function converts the checkout form to hidden fields, the result is that users with all required fields
     * will be enrolled into the course with a single click (no need to head to the checkout page) and users
     * who are missing required information will be directed to the checkout page.
     *
     * @since 5.0.0
     * @since 5.1.0 Specifiy to pass the new 3rd param to the `llms_forms_block_to_field_settings` filter callback.
     * @since 5.9.0 Fix php 8.1 deprecation warnings when `get_form_fields()` returns `false`.
     * @since 7.0.0 Retrieve and use the free checkout redirect URL as not encoded.
     *
     * @param LLMS_Access_Plan $plan Access plan being used for enrollment.
     * @return array[] List of LLMS_Form_Field settings arrays.
     */
    public function get_free_enroll_form_fields( $plan ) {
 
        // Convert all fields to hidden fields and remove any fields hidden by LLMS block-level visibility settings.
        add_filter( 'llms_forms_block_to_field_settings', array( $this, 'prepare_field_for_free_enroll_form' ), 999, 3 );
        $fields = $this->get_form_fields( 'checkout', compact( 'plan' ) );
        remove_filter( 'llms_forms_block_to_field_settings', array( $this, 'prepare_field_for_free_enroll_form' ), 999, 3 );
 
        // If no fields are found, ensure we add to an array instead of casting false to an array (causing a PHP 8.1 deprecation warning).
        $fields = ! is_array( $fields ) ? array() : $fields;
 
        // Add additional fields required for form processing.
        $fields[] = array(
            'name'           => 'free_checkout_redirect',
            'type'           => 'hidden',
            'value'          => $plan->get_redirection_url( false ),
            'data_store_key' => false,
        );
 
        $fields[] = array(
            'id'             => 'llms-plan-id',
            'name'           => 'llms_plan_id',
            'type'           => 'hidden',
            'value'          => $plan->get( 'id' ),
            'data_store_key' => false,
        );
 
        /**
         * Filter the list of LLMS_Form_Fields used to generate the "free enrollment" form
         *
         * @since 5.0.0
         *
         * @param array[]          $fields List of LLMS_Form_Field settings arrays.
         * @param LLMS_Access_Plan $plan   Access plan being used for enrollment.
         */
        return apply_filters( 'llms_forms_get_free_enroll_form_fields', $fields, $plan );
 
    }
 
    /**
     * Retrieve the HTML of form fields used for the "free enrollment" form
     *
     * @since 5.0.0
     *
     * @see LLMS_Forms::get_free_enroll_form_fields()
     *
     * @param LLMS_Access_Plan $plan Access plan being used for enrollment.
     * @return string
     */
    public function get_free_enroll_form_html( $plan ) {
 
        $html = '';
        foreach ( $this->get_free_enroll_form_fields( $plan ) as $field ) {
            $html .= llms_form_field( $field, false );
        }
 
        return $html;
 
    }
 
    /**
     * Retrieve information on all the available form locations.
     *
     * @since 5.0.0
     *
     * @return array[] {
     *     An associative array. The array key is the location ID and each array is a location definition array.
     *
     *     @type string  $name        The human-readable location name (as displayed on the admin panel).
     *     @type string  $description A description of the form (as displayed on the admin panel).
     *     @type string  $title       The form's post title. This is displayed to the end user when the "Show Form Title" option is enabled.
     *     @type array   $meta        An associative array of postmeta information for the form. The array key is the meta key and the value is the meta value.
     *     @type string  $template    A string used to generate the post content of the form post, usually retrieve from `LLMS_Form_Templates`.
     *     @type array   $meta        Array of meta data used when generating the form. The array key is the meta key and array value is the meta value.
     *     @type array[] $required    Array of arrays defining required fields for each form.
     * }
     */
    public function get_locations() {
 
        $locations = require LLMS_PLUGIN_DIR . 'includes/schemas/llms-form-locations.php';
 
        /**
         * Filter the available form locations.
         *
         * NOTE: Removing core forms (as well as modifying the ids / keys) may cause areas of LifterLMS to stop working.
         *
         * @since 5.0.0
         *
         * @param  array[] $locations Associative array of form location information.
         */
        return apply_filters( 'llms_forms_get_locations', $locations );
 
    }
 
    /**
     * Retrieve the forms post type name.
     *
     * @since 5.0.0
     *
     * @return string
     */
    public function get_post_type() {
        return $this->post_type_manager->post_type;
    }
 
    /**
     * Determine if a block is visible based on LifterLMS Visibility Settings.
     *
     * @since 5.0.0
     * @since 7.1.4 Fixed an issue running unit tests on PHP 7.4 and WordPress 6.2
     *              expecting `render_block()` returning a string while we were applying a filter
     *              that returned the boolean `true`.
     *
     * @param array $block Parsed block array.
     * @return bool
     */
    private function is_block_visible( $block ) {
 
        // Make the block return a non empty string if it's visible, it will already automatically return an empty string if it's invisible.
        add_filter( 'render_block', array( __CLASS__, '__return_string' ), 5 );
 
        // Don't run this class render function on the block during this test.
        remove_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 );
 
        // Render the block.
        $render = render_block( $block );
 
        // Cleanup / reapply filters.
        add_filter( 'render_block', array( $this, 'render_field_block' ), 10, 2 );
        remove_filter( 'render_block', array( __CLASS__, '__return_string' ), 5 );
 
        /**
         * Filter whether or not the block is visible.
         *
         * @since 5.0.0
         *
         * @param bool  $visible Whether or not the block is visible.
         * @param array $block   Parsed block array.
         */
        return apply_filters( 'llms_forms_is_block_visible', llms_parse_bool( $render ), $block );
 
    }
 
    /**
     * Determine if a block is visible in the list it's contained based on LifterLMS Visibility Settings
     *
     * Fall back on `$this->is_block_visible()` if empty `$block_list` is provided.
     *
     * @since 5.1.0
     *
     * @param array   $block      Parsed block array.
     * @param array[] $block_list The list of WP Block array `$block` comes from.
     * @return bool Returns `true` if `$block` (and all its parents) are visible. Returns `false` when `$block`
     *              or any of its parents are hidden or when `$block` is not found within `$block_list`.
     */
    public function is_block_visible_in_list( $block, $block_list ) {
 
        if ( empty( $block_list ) ) {
            return $this->is_block_visible( $block );
        }
 
        $path       = $this->get_block_path( $block, $block_list );
        $is_visible = ! empty( $path ); // Assume the block is visible until proven hidden, except when path is empty.
        foreach ( $path as $block ) {
            if ( ! $this->is_block_visible( $block ) ) {
                $is_visible = false;
                break;
            }
        }
 
        /**
         * Filter whether or not the block is visible in the list of blocks it's contained.
         *
         * @since 5.1.0
         *
         * @param bool    $is_visible Whether or not the block is visible.
         * @param array   $block      Parsed block array.
         * @param array[] $block_list The list of WP Block array `$block` comes from.
         */
        return apply_filters( 'llms_forms_is_block_visible', $is_visible, $block, $block_list );
 
    }
 
    /**
     * Returns a list of block parents plus the block itself in reverse order
     *
     * @since 5.1.0
     *
     * @param array   $block      Parsed block array.
     * @param array[] $block_list The list of WP Block array `$block` comes from.
     * @param int     $iterations Stores the number of iterations.
     * @return array[] List of WP_Block arrays or an empty array if `$block` cannot be found within `$block_list`.
     */
    private function get_block_path( $block, $block_list, $iterations = 0 ) {
 
        foreach ( $block_list as $_block ) {
 
            // Found the block.
            if ( $block === $_block ) {
                return array( $block );
            }
 
            // No innerblocks, proceed to the next block.
            if ( empty( $_block['innerBlocks'] ) ) {
                continue;
            }
 
            // Look in innerblocks for the block.
            foreach ( $_block['innerBlocks'] as $inner_block ) {
 
                // The inner block needs to be merged to the path.
                $to_merge = array( $inner_block );
 
                if ( $block === $inner_block ) { // Inner block is the one we're looking for.
                    $path     = array( $block );
                    $to_merge = array(); // Inner block equals the path, no need to merge it.
                } else {
                    $path = $this->get_block_path( $block, array( $inner_block ), $iterations + 1 );
                }
 
                if ( $path ) {
 
                    // First iteration, append first block too.
                    if ( ! $iterations ) {
                        $to_merge[] = $_block;
                    }
 
                    // Merge.
                    return array_merge( $path, $to_merge );
 
                }
            }
        }
 
        // Block not found in the list.
        return array();
 
    }
 
    /**
     * Returns a filtered version of `$block_list` containing only the passed `$block` and its parents.
     *
     * @since 5.1.0
     *
     * @param array   $block      Parsed block array.
     * @param array[] $block_list The list of WP Block array `$block` comes from.
     * @return array[] Filtered version of `$block_list` containing only the passed `$block` and its parents.
     *                 Or an empty array if `$block` cannot be found within `$block_list`.
     */
    private function get_block_tree( $block, $block_list ) {
 
        foreach ( $block_list as &$_block ) {
 
            // Found the block.
            if ( $block === $_block ) {
                return array( $block );
            }
 
            if ( ! empty( $_block['innerBlocks'] ) ) {
                $tree = $this->get_block_tree( $block, $_block['innerBlocks'] );
            }
 
            if ( ! empty( $tree ) ) { // Break as soon as the desired block is removed from one of the innerBlocks.
                if ( $_block['innerBlocks'] !== $tree ) { // Update innerBlocks/innerContent structure if needed.
                    $_block['innerBlocks'] = $tree;
                    // Update innerContent to reflect the innerBlocks changes = only 1 innerBlock.
                    $inner_block_in_content_index = 0;
                    foreach ( $_block['innerContent'] as $index => $chunk ) {
                        if ( ! is_string( $chunk ) && $inner_block_in_content_index++ ) {
                            unset( $_block['innerContent'][ $index ] );
                        }
                    }
                    // Re-index.
                    $_block['innerContent'] = array_values( $_block['innerContent'] );
                }
 
                return array( $_block );
            }
        }
 
        return array();
 
    }
 
    /**
     * Installation function to install core forms.
     *
     * @since 5.0.0
     *
     * @param bool $recreate Whether or not to recreate an existing form. This is passed to `LLMS_Forms::create()`.
     * @return WP_Post[] Array of created posts. Array key is the location id and array value is the WP_Post object.
     */
    public function install( $recreate = false ) {
 
        $installed = array();
 
        foreach ( array_keys( $this->get_locations() ) as $location ) {
            $installed[ $location ] = $this->create( $location, $recreate );
        }
 
        return $installed;
 
    }
 
    /**
     * Determines if a location is a valid & registered form location
     *
     * @since 5.0.0
     *
     * @param string $location The location id.
     * @return boolean
     */
    public function is_location_valid( $location ) {
        return in_array( $location, array_keys( $this->get_locations() ), true );
    }
 
    /**
     * Loads reusable blocks into a block list.
     *
     * A reusable block contains a reference to the block post, e.g. `<!-- wp:block {"ref":2198} /-->`,
     * which will be loaded during rendering.
     *
     * Dereferencing the reusable blocks allows the entire block list to be reviewed and to validate all form fields.
     * This function will replace each reusable block with the parsed blocks from its reference post.
     *
     * @since 5.0.0
     * @since 5.1.0 Access turned to public.
     *
     * @param array[] $blocks An array of blocks from `parse_blocks()`,
     *                        where each block is usually an array cast from `WP_Block_Parser_Block`.
     *
     * @return array[]
     */
    public function load_reusable_blocks( $blocks ) {
 
        $loaded = array();
 
        foreach ( $blocks as $block ) {
 
            // Skip blocks that are not reusable blocks.
            if ( 'core/block' === $block['blockName'] ) {
 
                // Skip reusable blocks that do not exist or are not published.
                $post = get_post( $block['attrs']['ref'] );
                if ( ! $post || 'publish' !== get_post_status( $post ) ) {
                    continue;
                }
 
                $loaded = array_merge( $loaded, $this->parse_blocks( $post->post_content ) );
                continue;
            }
 
            // Does this block's inner blocks have references to reusable blocks?
            if ( $block['innerBlocks'] ) {
                $block['innerBlocks'] = $this->load_reusable_blocks( $block['innerBlocks'] );
            }
 
            $loaded[] = $block;
        }
 
        return $loaded;
 
    }
 
    /**
     * Load form autosaves when previewing a form
     *
     * @since 5.0.0
     *
     * @param WP_Post|boolean $post WP_Post object for the llms_form post or `false` if no form found.
     * @return WP_Post|boolean
     */
    public function maybe_load_preview( $post ) {
 
        // No form post found.
        if ( ! is_object( $post ) ) {
            return $post;
        }
 
        // The `_set_preview()` method is marked as private but has existed since 2.7 and my guess is that we can use this safely.
        if ( ! function_exists( '_set_preview' ) ) {
            return $post;
        }
 
        $is_preview = ( is_preview() && current_user_can( $this->get_capability(), $post->ID ) );
 
        return $is_preview ? _set_preview( $post ) : $post;
 
    }
 
    /**
     * Parse the post_content of a form into a list of WP_Block arrays.
     *
     * This method parses the blocks, loads block data from any reusable blocks,
     * and cascades visibility attributes onto a block's innerBlocks.
     *
     * @since 5.0.0
     *
     * @param string $content Post content HTML.
     * @return array[] Array of parsed block arrays.
     */
    public function parse_blocks( $content ) {
 
        $blocks = parse_blocks( $content );
 
        $blocks = $this->load_reusable_blocks( $blocks );
 
        $blocks = $this->cascade_visibility_attrs( $blocks );
 
        return $blocks;
 
    }
 
    /**
     * Modifies a field for usage in the "free enrollment" checkout form
     *
     * If the block is not visible (according to LLMS block-level visibility settings)
     * it will return an empty array (signaling the field to be removed).
     *
     * Otherwise the block will be converted to a hidden field.
     *
     * This method is a filter callback and is intended for internal use only.
     *
     * Backwards incompatible changes and/or method removal may occur without notice.
     *
     * @since 5.0.0
     * @since 5.1.0 Added `$block_list` param.
     * @access private
     *
     * @param array   $attrs      LLMS_Form_Field settings array for the field.
     * @param array   $block      WP_Block settings array.
     * @param array[] $block_list The list of WP Block array `$block` comes from.
     * @return array
     */
    public function prepare_field_for_free_enroll_form( $attrs, $block, $block_list ) {
 
        if ( ! $this->is_block_visible_in_list( $block, $block_list ) ) {
            return array();
        }
 
        $attrs['type'] = 'hidden';
        return $attrs;
 
    }
 
    /**
     * Render form field blocks.
     *
     * @since 5.0.0
     * @since 5.9.0 Pass an empty string to `strpos()` instead of `null`.
     *
     * @param string $html  Block HTML.
     * @param array  $block Array of block information.
     * @return string
     */
    public function render_field_block( $html, $block ) {
 
        // Return HTML for any non llms/form-field blocks.
        if ( false === strpos( $block['blockName'] ?? '', 'llms/form-field-' ) ) {
            return $html;
        }
 
        if ( ! empty( $block['innerBlocks'] ) ) {
 
            $inner_blocks = array_map( 'render_block', $block['innerBlocks'] );
            return implode( "\n", $inner_blocks );
 
        }
 
        $attrs = $this->block_to_field_settings( $block );
 
        return llms_form_field( $attrs, false );

Top ↑

Methods Methods


Top ↑

Changelog Changelog

Changelog
Version Description
5.3.0 Replace singleton code with LLMS_Trait_Singleton.
5.0.0 Introduced.

Top ↑

User Contributed Notes User Contributed Notes

You must log in before being able to contribute a note or feedback.