From af931aa353433f8898eb74d35e1d2868d8ee6563 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lucas=20Garc=C3=ADa?= <lucas@codeccoop.org>
Date: Tue, 23 Apr 2024 20:25:12 +0200
Subject: [PATCH] feat: gf vat-id field

---
 abstract/class-integration.php                |   9 +-
 includes/fields/gf/iban/class-addon.php       |   6 +
 includes/fields/gf/iban/class-field.php       |  55 ++--
 includes/fields/gf/vat-id/class-addon.php     |  58 +++++
 .../fields/gf/vat-id/class-field-adapter.php  |  28 +++
 includes/fields/gf/vat-id/class-field.php     | 235 ++++++++++++++++++
 .../integrations/gf/class-integration.php     |  41 +--
 wpct-erp-forms.php                            |   9 +-
 8 files changed, 397 insertions(+), 44 deletions(-)
 create mode 100644 includes/fields/gf/vat-id/class-addon.php
 create mode 100644 includes/fields/gf/vat-id/class-field-adapter.php
 create mode 100644 includes/fields/gf/vat-id/class-field.php

diff --git a/abstract/class-integration.php b/abstract/class-integration.php
index 69f0caa..1ef2b14 100644
--- a/abstract/class-integration.php
+++ b/abstract/class-integration.php
@@ -16,15 +16,16 @@ abstract class Integration extends Singleton
     {
         foreach (static::$fields as $Field) {
             $field = $Field::get_instance();
+            add_action('init', function () use ($field) {
+                $field->init();
+            });
         }
+
+        add_action('init', [$this, 'init']);
     }
 
     public function init()
     {
-        foreach (static::$fields as $Field) {
-            $field = $Field::get_instance();
-            $field->init();
-        }
     }
 
     public function submit($payload, $endpoints, $files, $form_data)
diff --git a/includes/fields/gf/iban/class-addon.php b/includes/fields/gf/iban/class-addon.php
index 1d8fbdc..1123270 100644
--- a/includes/fields/gf/iban/class-addon.php
+++ b/includes/fields/gf/iban/class-addon.php
@@ -34,6 +34,12 @@ class Addon extends GFAddOn
         return self::$_instance;
     }
 
+	function __construct()
+	{
+		$this->_full_path = __FILE__;
+		parent::__construct();
+	}
+
     /**
      * Include the field early so it is available when entry exports are being performed.
      */
diff --git a/includes/fields/gf/iban/class-field.php b/includes/fields/gf/iban/class-field.php
index dfa96d5..ef8eef5 100644
--- a/includes/fields/gf/iban/class-field.php
+++ b/includes/fields/gf/iban/class-field.php
@@ -3,6 +3,7 @@
 namespace WPCT_ERP_FORMS\GF\Fields\Iban;
 
 use GF_Field;
+use Exception;
 
 class GFField extends GF_Field
 {
@@ -142,7 +143,7 @@ class GFField extends GF_Field
      *
      * @return array
      */
-    function get_form_editor_field_settings()
+    public function get_form_editor_field_settings()
     {
         return [
             'conditional_logic_field_setting',
@@ -193,8 +194,8 @@ class GFField extends GF_Field
             $html_input_type = 'password';
         }
 
-        $logic_event = !$is_form_editor && !$is_entry_detail ? $this->get_conditional_logic_event('keyup') : '';
-        $id = (int)$this->id;
+        $logic_event = ''; // !$is_form_editor && !$is_entry_detail ? $this->get_conditional_logic_event('keyup') : '';
+        $id = (int) $this->id;
         $field_id = $is_entry_detail || $is_form_editor || $form_id == 0 ? "input_$id" : 'input_' . $form_id . "_$id";
 
         $value = esc_attr($value);
@@ -218,15 +219,18 @@ class GFField extends GF_Field
      */
     public function validate($value, $form)
     {
-        if (strlen($value) < 5) return false;
-        $value = strtolower(str_replace(' ', '', $value));
-
+        try {
+            if (strlen($value) < 5) {
+                throw new Exception();
+            }
+            $value = strtolower(str_replace(' ', '', $value));
 
-        $country_exists = array_key_exists(substr($value, 0, 2), $this->_countries);
-        $country_conform = strlen($value) == $this->_countries[substr($value, 0, 2)];
+            $country_exists = array_key_exists(substr($value, 0, 2), $this->_countries);
+            $country_conform = strlen($value) == $this->_countries[substr($value, 0, 2)];
 
-        try {
-            if (!($country_exists && $country_conform)) throw new Exception();
+            if (!($country_exists && $country_conform)) {
+                throw new Exception();
+            }
 
             $moved_char = substr($value, 4) . substr($value, 0, 4);
             $move_char_array = str_split($moved_char);
@@ -234,7 +238,9 @@ class GFField extends GF_Field
 
             foreach ($move_char_array as $key => $val) {
                 if (!is_numeric($move_char_array[$key])) {
-                    if (!isset($this->_chars[$val])) throw new Exception();
+                    if (!isset($this->_chars[$val])) {
+                        throw new Exception();
+                    }
                     $move_char_array[$key] = $this->_chars[$val];
                 }
 
@@ -244,13 +250,28 @@ class GFField extends GF_Field
             if (bcmod($new_string, '97') != 1) {
                 throw new Exception();
             }
-        } catch (Exception) {
+        } catch (Exception $e) {
             $this->failed_validation = true;
-            if (!empty($this->errorMessage)) {
-                $this->validation_message = $this->errorMessage;
-            } else {
-                $this->validation_message = __('The IABN you inserted is not valid.');
-            }
+            $this->validation_message = empty($this->errorMessage) ? __('The IBAN you\'ve inserted is not valid.', 'wpct-erp-forms') : $this->errorMessage;
         }
     }
+
+    public function get_form_inline_script_on_page_render($form)
+    {
+        ob_start();
+        ?>
+		const input = document.querySelector('#input_<?= $form['id'] ?>_<?= $this->id ?>');
+		input.addEventListener("input", ({ target }) => {
+			const value = String(target.value);
+			const chars = value.split("").filter((c) => c !== " ");
+			target.value = chars.reduce((repr, char, i) => {
+				if (i % 4 === 0) {
+					char = " " + char;
+				}
+				return repr + char;
+			});
+		});
+		<?php
+        return ob_get_clean();
+    }
 }
diff --git a/includes/fields/gf/vat-id/class-addon.php b/includes/fields/gf/vat-id/class-addon.php
new file mode 100644
index 0000000..fb297e7
--- /dev/null
+++ b/includes/fields/gf/vat-id/class-addon.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace WPCT_ERP_FORMS\GF\Fields\VatID;
+
+use GFForms;
+use GFAddOn;
+use GF_Fields;
+
+GFForms::include_addon_framework();
+
+class Addon extends GFAddOn
+{
+    protected $_version = '1.0';
+    protected $_slug = 'wpct-erp-forms-vat-id-field';
+    protected $_title = 'Gravity Forms VatID validated text field';
+    protected $_short_title = 'VatID field';
+	protected $_full_path;
+
+    /**
+     * @var object $_instance If available, contains an instance of this class.
+     */
+    private static $_instance = null;
+
+    /**
+     * Returns an instance of this class, and stores it in the $_instance property.
+     *
+     * @return object $_instance An instance of this class.
+     */
+    public static function get_instance()
+    {
+        if (self::$_instance == null) {
+            self::$_instance = new self();
+        }
+
+        return self::$_instance;
+    }
+
+    public function __construct()
+    {
+        $this->_full_path = __FILE__;
+        parent::__construct();
+    }
+
+    /**
+     * Include the field early so it is available when entry exports are being performed.
+     */
+    public function pre_init()
+    {
+        parent::pre_init();
+        if (
+            $this->is_gravityforms_supported() &&
+            class_exists('GF_Field') &&
+            class_exists('GF_Fields')
+        ) {
+            GF_Fields::register(new GFField());
+        }
+    }
+}
diff --git a/includes/fields/gf/vat-id/class-field-adapter.php b/includes/fields/gf/vat-id/class-field-adapter.php
new file mode 100644
index 0000000..2a23769
--- /dev/null
+++ b/includes/fields/gf/vat-id/class-field-adapter.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace WPCT_ERP_FORMS\GF\Fields\VatID;
+
+use WPCT_ERP_FORMS\Abstract\Field as BaseField;
+
+use GFAddOn;
+
+require_once 'class-addon.php';
+require_once 'class-field.php';
+
+class FieldAdapter extends BaseField
+{
+    public function __construct()
+    {
+        add_action('gform_loaded', [$this, 'register']);
+    }
+
+    public function register()
+    {
+        if (!method_exists('GFForms', 'include_addon_framework')) return;
+        GFAddOn::register(Addon::class);
+    }
+
+    public function init()
+    {
+    }
+}
diff --git a/includes/fields/gf/vat-id/class-field.php b/includes/fields/gf/vat-id/class-field.php
new file mode 100644
index 0000000..913e1bf
--- /dev/null
+++ b/includes/fields/gf/vat-id/class-field.php
@@ -0,0 +1,235 @@
+<?php
+
+namespace WPCT_ERP_FORMS\GF\Fields\VatID;
+
+use Exception;
+use GF_Field;
+
+class GFField extends GF_Field
+{
+    private static $_dni_regex = '/^(\d{8})([A-Z])$/';
+    private static $_cif_regex = '/^([ABCDEFGHJKLMNPQRSUVW])(\d{7})([0-9A-J])$/';
+    private static $_nie_regex = '/^[XYZ]\d{7,8}[A-Z]$/';
+
+    /**
+     * @var string $type The field type.
+     */
+    public $type = 'vat-id-field';
+
+    /**
+     * Return the field title, for use in the form editor.
+     *
+     * @return string
+     */
+    public function get_form_editor_field_title()
+    {
+        return esc_attr__('VAT ID');
+    }
+
+    /**
+     * Assign the field button to the Advanced Fields group.
+     *
+     * @return array
+     */
+    public function get_form_editor_button()
+    {
+        return [
+            'group' => 'advanced_fields',
+            'text'  => $this->get_form_editor_field_title(),
+        ];
+    }
+
+    /**
+     * The settings which should be available on the field in the form editor.
+     *
+     * @return array
+     */
+    public function get_form_editor_field_settings()
+    {
+        return [
+            'conditional_logic_field_setting',
+            'error_message_setting',
+            'label_setting',
+            'label_placement_setting',
+            'admin_label_setting',
+            'size_setting',
+            'password_field_setting',
+            'rules_setting',
+            'visibility_setting',
+            'duplicate_setting',
+            'default_value_setting',
+            'placeholder_setting',
+            'description_setting',
+            'css_class_setting',
+        ];
+    }
+
+    /**
+     * Enable this field for use with conditional logic.
+     *
+     * @return bool
+     */
+    public function is_conditional_logic_supported()
+    {
+        return true;
+    }
+
+    /**
+     * Define the fields inner markup.
+     *
+     * @param array        $form The Form Object currently being processed.
+     * @param string|array $value The field value. From default/dynamic population, $_POST, or a resumed incomplete submission.
+     * @param null|array   $entry Null or the Entry Object currently being edited.
+     *
+     * @return string
+     */
+    public function get_field_input($form, $value = '', $entry = null)
+    {
+        $form_id = absint($form['id']);
+        $is_entry_detail = $this->is_entry_detail();
+        $is_form_editor  = $this->is_form_editor();
+
+        $html_input_type = 'text';
+
+        if ($this->enablePasswordInput && !$is_entry_detail) {
+            $html_input_type = 'password';
+        }
+
+        $logic_event = ''; // !$is_form_editor && !$is_entry_detail ? $this->get_conditional_logic_event('keyup') : '';
+        $id = (int) $this->id;
+        $field_id = $is_entry_detail || $is_form_editor || $form_id == 0 ? "input_$id" : 'input_' . $form_id . "_$id";
+
+        $value = esc_attr($value);
+        $size = $this->size;
+        $class_suffix = $is_entry_detail ? '_admin' : '';
+        $class = $size . $class_suffix;
+
+        $tabindex = $this->get_tabindex();
+        $disabled_text = $is_form_editor ? 'disabled="disabled"' : '';
+        $placeholder_attribute = $this->get_field_placeholder_attribute();
+        $required_attribute = $this->isRequired ? 'aria-required="true"' : '';
+        $invalid_attribute = $this->failed_validation ? 'aria-invalid="true"' : 'aria-invalid="false"';
+
+        $input = "<input name='input_{$id}' id='{$field_id}' type='{$html_input_type}' value='{$value}' class='{$class}' {$tabindex} {$logic_event} {$placeholder_attribute} {$required_attribute} {$invalid_attribute} {$disabled_text}/>";
+
+        return sprintf("<div class='ginput_container ginput_container_text'>%s</div>", $input);
+    }
+
+    /**
+     * Validate Field
+     */
+    public function validate($value, $form)
+    {
+        try {
+            if (strlen($value) < 5) {
+                throw new Exception();
+            }
+
+            $value = strtolower(str_replace(' ', '', $value));
+            $value = preg_replace('/\s/', '', strtoupper($value));
+
+			$valid = false;
+            $type = $this->id_type($value);
+            switch ($type) {
+                case 'dni':
+                    $valid = $this->validate_dni($value);
+                    break;
+                case 'nie':
+                    $valid = $this->validate_nie($value);
+                    break;
+                case 'cif':
+                    $valid = $this->validate_cif($value);
+                    break;
+            }
+
+            if (!$valid) {
+                throw new Exception();
+            }
+        } catch (Exception $e) {
+            $this->failed_validation = true;
+            $this->validation_message = empty($this->errorMessage) ? __('The VAT number you\'ve inserted is not valid.', 'wpct-erp-forms') : $this->errorMessage;
+        }
+    }
+
+    private function id_type($value)
+    {
+        if (preg_match(GFField::$_dni_regex, $value)) {
+            return 'dni';
+        } elseif (preg_match(GFField::$_nie_regex, $value)) {
+            return 'nie';
+        } elseif (preg_match(GFField::$_cif_regex, $value)) {
+            return 'cif';
+        }
+    }
+
+    private function validate_dni($value)
+    {
+        $dni_letters = 'TRWAGMYFPDXBNJZSQVHLCKE';
+        $number = (int) substr($value, 0, 8);
+        $index = $number % 23;
+        $letter = substr($dni_letters, $index, 1);
+
+        return $letter == substr($value, 8, 9);
+    }
+
+    private function validate_nie($value)
+    {
+        $nie_prefix = substr($value, 0, 1);
+
+        switch ($nie_prefix) {
+            case  'X':
+                $nie_prefix = 0;
+                break;
+            case 'Y':
+                $nie_prefix = 1;
+                break;
+            case 'Z':
+                $nie_prefix = 2;
+                break;
+        }
+
+        return $this->validate_dni($nie_prefix . substr($value, 1, 9));
+    }
+
+    private function validate_cif($value)
+    {
+        preg_match(GFField::$_cif_regex, $value, $matches);
+        $letter = $matches[1];
+        $number = $matches[2];
+        $control = $matches[3];
+
+        $even_sum = 0;
+        $odd_sum = 0;
+        $n = null;
+
+        for ($i = 0; $i < strlen($number); $i++) {
+            $n = (int) $number[$i];
+
+            if ($i % 2 === 0) {
+                // Odd positions are multiplied first.
+                $n *= 2;
+                // If the multiplication is bigger than 10 we need to adjust
+                $odd_sum += $n < 10 ? $n : $n - 9;
+
+                // Even positions
+                // Just sum them
+            } else {
+                $even_sum += $n;
+            }
+        }
+
+        $control_digit = 10 - (int) substr(strval($even_sum +  $odd_sum), -1);
+        $control_letter = substr('JABCDEFGHI', $control_digit, 1);
+
+        if (preg_match('/[ABEH]/', $letter)) {
+            // Control must be a digit
+            return (int) $control === $control_digit;
+        } elseif (preg_match('/[KPQS]/', $letter)) {
+            // Control must be a letter
+            return (string) $control === $control_letter;
+        } else {
+            // Can be either
+            return (int) $control === $control_digit || (string) $control === $control_letter;
+        }
+    }
+}
diff --git a/includes/integrations/gf/class-integration.php b/includes/integrations/gf/class-integration.php
index 064d267..d5e6136 100644
--- a/includes/integrations/gf/class-integration.php
+++ b/includes/integrations/gf/class-integration.php
@@ -6,17 +6,20 @@ use Exception;
 use TypeError;
 use WPCT_ERP_FORMS\Abstract\Integration as BaseIntegration;
 use WPCT_ERP_FORMS\GF\Fields\Iban\FieldAdapter as IbanField;
+use WPCT_ERP_FORMS\GF\Fields\VatID\FieldAdapter as VatIDField;
 
 require_once 'attachments.php';
 require_once 'fields-population.php';
 
 // Fields
 require_once dirname(__FILE__, 3) . '/fields/gf/iban/class-field-adapter.php';
+require_once dirname(__FILE__, 3) . '/fields/gf/vat-id/class-field-adapter.php';
 
 class Integration extends BaseIntegration
 {
     public static $fields = [
-        IbanField::class
+        IbanField::class,
+        VatIDField::class,
     ];
 
     protected function __construct()
@@ -24,6 +27,8 @@ class Integration extends BaseIntegration
         add_action('gform_after_submission', function ($entry, $form) {
             $this->do_submission($entry, $form);
         }, 10, 2);
+
+		parent::__construct();
     }
 
     public function serialize_form($form)
@@ -58,21 +63,21 @@ class Integration extends BaseIntegration
 
         $inputs = $field->get_entry_inputs();
         if (is_array($inputs)) {
-			$inputs = array_map(function ($input) {
-				return ['name' => $input['name'], 'label' => $input['label'], 'id' => $input['id']];
-			}, array_filter($inputs, function ($input) {
-				return $input['name'];
-			}));
-		} else {
-			$inputs = [];
-		}
-
-		$options = [];
-		if (is_array($field->choices)) {
-			$options = array_map(function ($opt) {
-				return ['value' => $opt['value'], 'label' => $opt['text']];
-			}, $field->choices);
-		}
+            $inputs = array_map(function ($input) {
+                return ['name' => $input['name'], 'label' => $input['label'], 'id' => $input['id']];
+            }, array_filter($inputs, function ($input) {
+                return $input['name'];
+            }));
+        } else {
+            $inputs = [];
+        }
+
+        $options = [];
+        if (is_array($field->choices)) {
+            $options = array_map(function ($opt) {
+                return ['value' => $opt['value'], 'label' => $opt['text']];
+            }, $field->choices);
+        }
 
         return [
             'id' => $field->id,
@@ -81,8 +86,8 @@ class Integration extends BaseIntegration
             'label' => $field->label,
             'required' => $field->isRequired,
             'options' => $options,
-			'inputs' => $inputs,
-			'conditional' => is_array($field->conditionalLogic) && $field->conditionalLogic['enabled'],
+            'inputs' => $inputs,
+            'conditional' => is_array($field->conditionalLogic) && $field->conditionalLogic['enabled'],
         ];
     }
 
diff --git a/wpct-erp-forms.php b/wpct-erp-forms.php
index aa31f03..eac352d 100755
--- a/wpct-erp-forms.php
+++ b/wpct-erp-forms.php
@@ -36,6 +36,9 @@ require_once 'includes/class-settings.php';
 require_once 'custom-blocks/form/form.php';
 require_once 'custom-blocks/form-control/form-control.php';
 
+require_once 'includes/fields/gf/iban/class-field-adapter.php';
+require_once 'includes/fields/gf/vat-id/class-field-adapter.php';
+
 class Wpct_Erp_Forms extends Abstract\Plugin
 {
     private $_integrations = [];
@@ -61,10 +64,6 @@ class Wpct_Erp_Forms extends Abstract\Plugin
 
     public function init()
     {
-        foreach ($this->_integrations as $integration) {
-            $integration->init();
-        }
-
         add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']);
     }
 
@@ -106,4 +105,4 @@ class Wpct_Erp_Forms extends Abstract\Plugin
 
 add_action('plugins_loaded', function () {
     $plugin = Wpct_Erp_Forms::get_instance();
-}, 10);
+}, 5);
-- 
GitLab