From 6a18d031d42ea5c08e408d7860d41016f2557022 Mon Sep 17 00:00:00 2001
From: Matthieu Benoist <matthieu.benoist@randstaddigital.fr>
Date: Fri, 2 Feb 2024 11:49:08 +0100
Subject: [PATCH] repository update

---
 app/acfField/GLCaptchEtatField.class.php      | 174 ++++++++++++
 app/admin/CaptchEtatAdmin.class.php           | 263 ++++++++++++++++++
 app/forms/CaptchEtatForms.class.php           | 188 +++++++++++++
 app/index.php                                 |   2 +
 app/service/CaptchEtatService.class.php       | 225 +++++++++++++++
 app/wpcPlugin.class.php                       | 212 ++++++++++++++
 assets/scripts/gl-captchetat-form.js          |  15 +
 .../jquery-capcha/jquery-captcha.min.js       |   2 +
 .../jquery-capcha/jquery-captcha.min.js.map   |   1 +
 assets/styles/acf.css                         |   9 +
 assets/styles/styles.css                      |  11 +
 composer.json                                 |  13 +
 index.php                                     |   2 +
 languages/wp-capchetat.pot                    |   0
 wp-captchetat.php                             |  71 +++++
 15 files changed, 1188 insertions(+)
 create mode 100644 app/acfField/GLCaptchEtatField.class.php
 create mode 100644 app/admin/CaptchEtatAdmin.class.php
 create mode 100644 app/forms/CaptchEtatForms.class.php
 create mode 100644 app/index.php
 create mode 100644 app/service/CaptchEtatService.class.php
 create mode 100644 app/wpcPlugin.class.php
 create mode 100644 assets/scripts/gl-captchetat-form.js
 create mode 100644 assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js
 create mode 100644 assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js.map
 create mode 100644 assets/styles/acf.css
 create mode 100644 assets/styles/styles.css
 create mode 100644 composer.json
 create mode 100644 index.php
 create mode 100644 languages/wp-capchetat.pot
 create mode 100644 wp-captchetat.php

diff --git a/app/acfField/GLCaptchEtatField.class.php b/app/acfField/GLCaptchEtatField.class.php
new file mode 100644
index 0000000..021b14d
--- /dev/null
+++ b/app/acfField/GLCaptchEtatField.class.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace CaptchEtat\AcfField;
+
+use acf_field;
+use CaptchEtat\Service\CaptchEtatService;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
+class GLCaptchEtatField extends acf_field
+{  
+	protected $ceService;
+	protected $version = '1.0';
+	protected $rand;
+	protected $key = '';
+
+	protected static $flag = false;
+
+	protected static function __init(): void
+	{
+		add_action ('init', function() {acf_register_field_type(self::class); } );
+	}
+
+	public function __construct()
+	{
+		$this->rand = rand();
+		$this->ceService = CaptchEtatService::register();
+
+		$this->name  = 'CaptchETAT';
+		$this->label = __('Validateur captcheta','GLCAPTCHETAT');
+		$this->category = 'Custom';
+		$this->defaults = [];
+
+		add_filter('acf/load_field/type=CaptchETAT', function($field) {
+			$this->key = $field['key'];
+			$field['key'] = 'captchaFormulaireExtInput' . $this->rand;
+			$field['prefix'] = '';
+			
+			return $field;
+		},200,1);
+
+		add_filter('acf/prepare_field/type=CaptchETAT', function($field) {
+
+			$field['key'] = $this->key;
+			$field['prefix'] = 'acf';
+		
+			return $field;
+		},200,1);
+
+		add_filter('acf/validate_save_post', array($this, 'validate_save_recaptcha_post'), 10, 0);
+
+		parent::__construct();
+
+	}
+
+	/**
+	 * Settings to display when users configure a field of this type.
+	 *
+	 * These settings appear on the ACF “Edit Field Group” admin page when
+	 * setting up the field.
+	 *
+	 * @param array $field
+	 * @return void
+	 */
+	public function render_field_settings( $field ) 
+	{
+		/*
+		 * Repeat for each setting you wish to display for this field type.
+		 */
+		acf_render_field_setting(
+			$field,
+			array(
+				'label'			=> $this->label,
+				'instructions'	=> __( 'Adds a CaptchEtat field on your form','GLCAPTCHETAT' ),
+				'name'			=> $this->name,
+			)
+		);
+
+		// To render field settings on other tabs in ACF 6.0+:
+		// https://www.advancedcustomfields.com/resources/adding-custom-settings-fields/#moving-field-setting
+	}
+
+	/**
+	 * HTML content to show when a publisher edits the field on the edit screen.
+	 *
+	 * @param array $field The field settings and values.
+	 * @return void
+	 */
+	public function render_field( $field ) 
+	{
+		if (!is_admin()) {
+			if ($this->ceService->getShowCaptcha()) {
+				$html = '<div id="botdetect-captcha'.rand().'" data-captchastylename="captchaFR"></div>
+						<input class="captchetat-acf" id="acf-captchaFormulaireExtInput'.$this->rand.'" name="acf['.$field['key'].']" placeholder="Tapez le code lu ou entendu" type="text" required/>
+						<input id="captchaId'.$this->rand.'" name="captchaId" type="hidden" />';
+			} else {
+				$html =
+					'<div>'
+					. __('Le service de captcha est indisponible.', 'acf-recaptcha').
+					'</div>';
+			}
+		}
+		else {
+			$html = '<div>' .__('Le service de sécurité est désactivé depuis l\'interface d\'administration.', 'acf-recaptcha').'</div>';
+		}
+
+		echo $html;
+
+	}
+
+	/**
+	 * Enqueues CSS and JavaScript needed by HTML in the render_field() method.
+	 *
+	 * Callback for admin_enqueue_script.
+	 *
+	 * @return void
+	 */
+	public function input_admin_enqueue_scripts() 
+	{
+		$url = WP_CAPTCHETAT_PLUGIN_URL . '/assets' ;
+
+		wp_register_style(
+			'WP-CAPTCHETAT',
+			"{$url}/styles/acf.css"
+		);
+		wp_enqueue_style('WP-CAPTCHETAT');
+	}
+
+
+
+
+	function validate_save_recaptcha_post() 
+	{
+		session_start();
+		// Determine if the form contains any 'captchetat' field types.
+		$form_contains_captcha = false;
+		$valid = false;
+		//if the service captchetat is not available the captcha is not shown so no validation needed
+		if (isset($_SESSION['captchETAT']) && $_SESSION['captchETAT'] == $_POST['BDC_VCID_captchaFR']) {
+			unset($_SESSION['captchETAT']);
+			return;
+		}
+
+		foreach ($_POST['acf'] as $field_key => $value) {
+			$field = acf_get_field($field_key);
+			//			var_dump($field);
+			if ($field['type'] === 'CaptchETAT') {
+				$form_contains_captcha = true; // we have a captcha
+				$valid = $this->ceService->validateCaptcha($value,$_POST['BDC_VCID_captchaFR']); 
+				if ($valid == true) { 
+					unset($_POST['acf']['$field_key']);
+					if (defined('DOING_AJAX') && DOING_AJAX) {
+						$_SESSION['captchETAT'] = $_POST['BDC_VCID_captchaFR'];
+					}
+					return;
+				}
+			}
+		}
+
+		// Don't handle forms without the flag or any recaptcha fields.
+		if ( !$form_contains_captcha) {
+			return;
+		}
+
+
+		if (!$valid) {
+			acf_add_validation_error('', __('La valeur du captcha saisie est invalide ou expirée. Merci de rééssayer.', 'acf-recaptcha'));
+		}
+		return;
+	}
+}
diff --git a/app/admin/CaptchEtatAdmin.class.php b/app/admin/CaptchEtatAdmin.class.php
new file mode 100644
index 0000000..70794ca
--- /dev/null
+++ b/app/admin/CaptchEtatAdmin.class.php
@@ -0,0 +1,263 @@
+<?php
+
+namespace CaptchEtat\Admin;
+
+use CaptchEtat;
+use CaptchEtat\WpcPlugin;
+use WP_Error;
+
+class CaptchEtatAdmin
+{
+    /**
+     * CaptchEtatAdmin service
+     *
+     * @var CaptchEtatAdmin
+     */
+    static private $_instance;
+
+    private $wpcPlugin;
+
+    /**
+     * @var string|array
+     */
+    private $settings;
+
+    /**
+     * Registering singleton
+     *
+     * @return CaptchEtatAdmin
+     */
+    static public function register()
+    {
+        if (!isset(self::$_instance)) {
+            self::$_instance = new self();
+        }
+
+        return self::$_instance;
+    }
+
+
+    protected function __construct()
+    {
+        $this->wpcPlugin = WpcPlugin::register();
+        $this->initSettingsActions();
+    }
+
+
+    /**
+     * register glcapt_settings_init to the admin_init action hook
+     */
+    private function initSettingsActions()
+    {
+
+        add_action('admin_menu', function () {
+            $this->addOptionsPage();
+        });
+
+        add_action('admin_init', function () {
+            $this->settingsAdminInit();
+        });
+
+    }
+
+    function settingsAdminInit()
+    {
+        if (!get_option('glcapt_settings')) {
+            add_option('glcapt_settings');
+        }
+        // register a new setting for "glcapt" page
+        register_setting(
+            'glcapt_option_group',
+            'glcapt_settings'
+        );
+
+        // register a new section in the "glcapt" page
+        add_settings_section(
+            'glcapt_settings_section',
+            'Options pour les appels et l\'intégration de l\'API Captchetat',
+            function () {
+                $this->registerSettingsSection();
+            },
+            'glcapt'
+        );
+
+        // register a new field in the "glcapt_settings_section" section, inside the "glcapt" page
+        add_settings_field(
+            'glcapt_client_id',
+            'Client id',
+            function () {
+                $this->setFieldHtmlClientId();
+            },
+            'glcapt',
+            'glcapt_settings_section'
+        );
+
+        add_settings_field(
+            'glcapt_client_secret',
+            'Client Secret',
+            function () {
+                $this->setFieldHtmlClientSecret();
+            },
+            'glcapt',
+            'glcapt_settings_section'
+        );
+
+        add_settings_field(
+            'glcapt_api_url',
+            'URL de l\'API',
+            function () {
+                $this->setFieldHtmlApiUrl('glcapt_api_url', 'glcapt_settings[glcapt_api_url]');
+            },
+            'glcapt',
+            'glcapt_settings_section'
+        );
+
+        add_settings_field(
+            'glcapt_token_url',
+            'URL de l\'API Oauth',
+            function () {
+                $this->setFieldHtmlApiUrl('glcapt_token_url', 'glcapt_settings[glcapt_token_url]');
+            },
+            'glcapt',
+            'glcapt_settings_section'
+        );
+
+        add_settings_section(
+            'glcapt_forms_section',
+            'Sélection des formulaires où activer le widget Captchetat',
+            function () {
+                $this->registerFormsSection();
+            },
+            'glcapt'
+        );
+
+        add_settings_field(
+            'glcapt_login_form',
+            'formulaire de login',
+            function () {
+                $this->setFieldHtmlFormsCheckbox('glcapt_login_form','glcapt_settings[glcapt_forms][]');
+            },
+            'glcapt',
+            'glcapt_forms_section'
+        );
+
+        add_settings_field(
+            'glcapt_comment_form',
+            'formulaire de commentaire',
+            function () {
+                $this->setFieldHtmlFormsCheckbox('glcapt_comment_form','glcapt_settings[glcapt_forms][]');
+            },
+            'glcapt',
+            'glcapt_forms_section'
+        );
+
+    }
+
+    /**
+     * callback functions
+     */
+
+    // section content cb
+    function registerSettingsSection()
+    {
+        echo '<p>Plugin pour intégrer la solution Captchetat du gouvernement français.</p>';
+    }
+
+    function registerFormsSection()
+    {
+        //todo
+    }
+
+    // field content cb
+    function setFieldHtmlClientId()
+    {
+        // get the value of the setting we've registered with register_setting()
+        if (!empty($this->settings["glcapt_client_id"])) {
+            $setting = esc_attr($this->settings["glcapt_client_id"]);
+        } else {
+            $setting = '';
+        }
+?>
+        <input type="text" id="glcapt_client_id" name="glcapt_settings[glcapt_client_id]" value="<?php echo $setting; ?>">
+    <?php
+    }
+
+    // field content cb
+    function setFieldHtmlClientSecret()
+    {
+        // get the value of the setting we've registered with register_setting()
+        if (!empty($this->settings["glcapt_client_secret"])) {
+            $setting = esc_attr($this->settings["glcapt_client_secret"]);
+        } else {
+            $setting = '';
+        }
+    ?>
+        <input type="text" id="glcapt_client_secret" name="glcapt_settings[glcapt_client_secret]" value="<?php echo $setting; ?>">
+    <?php
+    }
+
+    function setFieldHtmlApiUrl($idAttr, $nameAttr) {
+        if (!empty($this->settings[$idAttr])) {
+            $setting = sanitize_url($this->settings[$idAttr]);
+        } else {
+            $setting = '';
+        }
+    ?>
+        <input type="text" id="<?php echo $idAttr; ?>" name="<?php echo $nameAttr; ?>" value="<?php echo $setting; ?>">
+    <?php
+    }
+
+    function setFieldHtmlFormsCheckbox($idAttr, $nameAttr) {
+        if (!empty($this->settings['glcapt_forms']) && in_array($idAttr,$this->settings['glcapt_forms'])) {
+            $checked = 'checked';
+
+        } else {
+            $checked = '';
+        }
+        ?> 
+        <input type="checkbox" id="<?php echo $idAttr; ?>" name="<?php echo $nameAttr; ?>" value="<?php echo $idAttr; ?>" <?php echo $checked; ?>>
+        <?php
+    }
+
+    function generateOptionsPage()
+    {
+        // check user capabilities
+        if (!current_user_can('manage_options')) {
+            return;
+        }
+
+        $this->settings = get_option('glcapt_settings');
+
+    ?>
+        <div class="wrap">
+            <h1><?php echo esc_html(get_admin_page_title()); ?></h1>
+            <form action="options.php" method="post" enctype="multipart/form-data">
+                <?php
+                // output security fields for the registered setting "glcapt_options"
+                settings_fields('glcapt_option_group');
+                // output setting sections and their fields
+                // (sections are registered for "glcapt", each field is registered to a specific section)
+                do_settings_sections('glcapt');
+                // output save settings button
+                submit_button(__('Enregister', 'textdomain'));
+                ?>
+            </form>
+        </div>
+<?php
+    }
+
+    function addOptionsPage()
+    {
+        add_submenu_page(
+            'tools.php',
+            'Captchetat plugin by métropole de Lyon',
+            'Captchetat plugin',
+            'manage_options',
+            'glcapt',
+            function () {
+                $this->generateOptionsPage();
+            }
+        );
+    }
+
+}
diff --git a/app/forms/CaptchEtatForms.class.php b/app/forms/CaptchEtatForms.class.php
new file mode 100644
index 0000000..5b611ef
--- /dev/null
+++ b/app/forms/CaptchEtatForms.class.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace CaptchEtat\Forms;
+
+use CaptchEtat\Service\CaptchEtatService;
+use CaptchEtat\WpcPlugin;
+use WP_Error;
+use WP_REST_Server;
+use WP_User;
+
+class CaptchEtatForms
+{
+    /**
+     * CaptchEtatForms service
+     *
+     * @var CaptchEtatForms
+     */
+    static private $_instance;
+
+    private $wpcPlugin;
+
+    private $wpcService;
+
+    /**
+     * @var string|array
+     */
+    private $settings;
+
+    /**
+     * Registering singleton
+     *
+     * @return CaptchEtatForms
+     */
+    static public function register(): CaptchEtatForms
+    {
+        if (!isset(self::$_instance)) {
+            self::$_instance = new self();
+        }
+
+        return self::$_instance;
+    }
+
+
+    protected function __construct()
+    {
+        $this->wpcPlugin = WpcPlugin::register();
+        $this->wpcService = CaptchEtatService::register();
+        $this->initFormsActions();
+    }
+
+
+    /**
+     * register glcapt_settings_init to the admin_init action hook
+     */
+    private function initFormsActions()
+    {
+        add_action('rest_api_init', function () {
+            $this->initGLCaptchetatPluginRestApi();
+        });
+        add_action( 'wp_footer', function () {
+            $this->addGLCaptchetaScripts();
+        } );
+
+        if (!empty($this->wpcPlugin->getFormsWithCaptchetat()) && $this->wpcService->getShowCaptcha()) {
+            
+            if (in_array('glcapt_login_form', $this->wpcPlugin->getFormsWithCaptchetat())) {
+                $this->initCaptchaLoginForm();
+            }
+
+            if (in_array('glcapt_comment_form', $this->wpcPlugin->getFormsWithCaptchetat())) {
+                $this->initCaptchaCommentForm();
+            }
+        }
+    }
+
+    private function initCaptchaLoginForm()
+    {
+        add_action('login_form', function () {
+            $this->getGLCaptchetatHtml();
+        });
+
+        add_action( 'login_footer', function () {
+            //si erreur de jsquery en page de login décommenter cette ligne
+            //wp_enqueue_script('jquery') ;
+            $this->addGLCaptchetaScripts();
+        } );
+
+        add_filter('wp_authenticate_user', function ($user) {
+            return $this->validateLoginCaptcha($user);
+        });
+    }
+    private function initCaptchaCommentForm()
+    {
+        add_action( 'comment_form_after_fields', function() {
+            $this->getGLCaptchetatHtml();
+        });
+        add_action( 'comment_form_logged_in_after', function() {
+            $this->getGLCaptchetatHtml();
+        });
+        add_action( 'pre_comment_on_post', function() {
+           $this->validateCommentCaptcha();
+        });
+       
+    }
+
+    private function addGLCaptchetaScripts() 
+    {
+         //captchetat library
+         wp_enqueue_script('captchetat_script', WP_CAPTCHETAT_PLUGIN_URL . 'assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js');
+         //our js 
+         wp_enqueue_script('captchetat_plugin_form_integration', WP_CAPTCHETAT_PLUGIN_URL . 'assets/scripts/gl-captchetat-form.js');
+         
+         wp_enqueue_style('captchetat_style', WP_CAPTCHETAT_PLUGIN_URL . 'assets/styles/styles.css');
+         wp_add_inline_script(
+             'captchetat_plugin_form_integration',
+             'const GLCAPTPARAMS = ' .
+                 json_encode(
+                     array(
+                         'backendUrl' => rest_url('glcapt/simple-captcha-endpoint')
+                     )
+                 ),
+             'before'
+         );
+    }
+
+    private function getGLCaptchetatHtml() 
+    {
+        ?>
+            <div id="botdetect-captcha" data-captchastylename="captchaFR"></div>
+            <input aria-label="Code de vérification" id="captchaFormulaireExtInput" class = "captchetat-input" name="userEnteredCaptchaCode" type="text" required placeholder="Tapez le code lu ou entendu" />
+            <input id="captchaId" name="captchaId" type="hidden" />
+        <?php
+    }
+
+    function validateLoginCaptcha( $user) 
+    {
+        //only apply extra validation for the main login page
+        if ($GLOBALS['pagenow'] !== 'wp-login.php') {
+            return $user;
+        }
+        if (is_wp_error($user) && isset($user->errors['empty_username']) && isset($user->errors['empty_password'])) {
+            return $user;
+        }
+        if (empty($_POST['captchaId'])) {
+            return new WP_Error('captcha_librairy_error', __('Problème avec le système de validation captchetat'));
+        } elseif (empty($_POST['userEnteredCaptchaCode'])) {
+            return new WP_Error('captcha_empty', __('Merci de saisir le code de validation'));
+        } elseif (!$this->wpcService->validateCaptcha($_POST['userEnteredCaptchaCode'], $_POST['captchaId'])) {
+            return new WP_Error('captcha_invalide', __('Le captcha saisi est invalide'));
+        } 
+        return $user;
+    }
+
+    private function validateCommentCaptcha() 
+    {
+        $error = new WP_ERROR;
+        if (empty($_POST['captchaId'])) {
+            $error->add('captcha_librairy_error', wp_kses_post(__('Problème avec le système de validation captchetat')));
+        } elseif (empty($_POST['userEnteredCaptchaCode'])) {
+            $error->add('captcha_empty', wp_kses_post(__('Merci de saisir le code (captcha) de validation')));
+        } elseif (!$this->wpcService->validateCaptcha($_POST['userEnteredCaptchaCode'], $_POST['captchaId'])) {
+            $error->add('captcha_invalide', wp_kses_post(__('Le captcha saisi est invalide')));
+        }
+        if ($error->has_errors()) {
+            wp_die($error, "erreur de captcha", ["back_link" => true]);
+        }
+    }
+
+    private function initGLCaptchetatPluginRestApi()
+    {
+        /* Custom endpoints for sessions */
+        register_rest_route(
+            'glcapt',
+            '/simple-captcha-endpoint',
+            [
+                'methods' => WP_REST_Server::READABLE,
+                'callback' => function () {
+                    $res = $this->wpcService->getCaptchaElement($_GET);
+                    header('content-type:' . $res['content-type']);
+                    echo $res['content'];
+                    die();
+                },
+                'permission_callback' => function() {return true;},
+
+            ]
+        );
+    }
+}
diff --git a/app/index.php b/app/index.php
new file mode 100644
index 0000000..3226752
--- /dev/null
+++ b/app/index.php
@@ -0,0 +1,2 @@
+<?php
+// ssssh
\ No newline at end of file
diff --git a/app/service/CaptchEtatService.class.php b/app/service/CaptchEtatService.class.php
new file mode 100644
index 0000000..926c3e4
--- /dev/null
+++ b/app/service/CaptchEtatService.class.php
@@ -0,0 +1,225 @@
+<?php
+
+namespace CaptchEtat\Service;
+
+use CaptchEtat\WpcPlugin;
+use WP_Error;
+use WP_Http_Curl;
+
+class CaptchEtatService
+{
+    /**
+     * CaptchEtatService service
+     *
+     * @var CaptchEtatService
+     */
+    static private  $_instance;
+
+    private $wpcPlugin;
+
+    private $wpCurl;
+
+    /**
+     * Registering singleton
+     *
+     * @return CaptchEtatService
+     */
+    static public function register()
+    {
+        if (!isset(self::$_instance)) {
+            self::$_instance = new self();
+        }
+
+        return self::$_instance;
+    }
+
+
+    protected function __construct()
+    {
+        $this->wpCurl = new WP_Http_Curl();
+        $this->wpcPlugin = WpcPlugin::register();
+    }
+
+    /**
+     * URL Calls internal method
+     *
+     * @param string $url
+     * @param string $method
+     * @param array $options
+     * @return array
+     * @throws \Exception
+     */
+    protected function _call($url, $method = 'GET', $options = []): array
+    {
+
+        if ($method === 'GET') {
+            $response = wp_remote_get($url, $options);
+        } elseif ($method === 'POST') {
+            $response = wp_remote_post($url, $options);
+        } else {
+            throw new \Exception(
+                "only GET and POST methods are allowed",
+                1
+            );
+        }
+
+        if (is_wp_error($response, WP_Error::class)) {
+            throw new \Exception(
+                implode('\r ' , $response->get_error_messages()),
+                1);
+        }
+        if ($response['response']['code'] === 200) {
+            return $response;
+        } else {
+            throw new \Exception(
+                'exeption de _call, code : ' . $response['response']['code'] . ", message : " . $response['response']['message'],
+                $response['response']['code']
+            );
+        }
+    }
+
+    /**
+     * Is CaptchEtat's API available ?
+     * 
+     * @param string $token
+     * 
+     * @return bool
+     */
+    public function getShowCaptcha(): bool
+    {
+        $showCaptcha = false;
+
+        try {
+            $additionalOptions = [
+                'headers' => [
+                    'Authorization' => 'Bearer ' . $this->getToken()
+                ],
+            ];
+
+            $this->_call($this->wpcPlugin->getApiURL() . '/healthcheck', 'GET', $additionalOptions);
+            $showCaptcha = true;
+
+        } catch (\Exception $e) {
+        }
+
+        return $showCaptcha;
+    }
+
+    /**
+     * retrieve API Token
+     * 
+     * @return string
+     */
+    public function getToken(): string
+    {
+
+        //first, verify if we have a token already registered in wp_options
+        $tokenData = $this->wpcPlugin->getTokenData();
+        //test s'il en existe déjà un valable
+        if (!empty($tokenData) && time() < $tokenData['expire']) {
+            return $tokenData['access_token'];
+        } 
+
+        try {
+            $apiParams = [
+                'grant_type' => 'client_credentials',
+                'client_id' => $this->wpcPlugin->getClientId(),
+                'client_secret' => $this->wpcPlugin->getClientSecret(),
+                'scope' => 'piste.captchetat'
+            ];
+     
+
+            $additionalOptions = [
+                'body' => $apiParams,
+                //'headers' =>['Content-Type' => 'application/x-www-form-urlencoded']
+            ];
+
+
+            $response = $this->_call($this->wpcPlugin->getTokenURL(), 'POST', $additionalOptions);
+
+            if (strncmp($response['headers']['Content-Type'], 'application/json', strlen('application/json')) === 0) {
+                $return = json_decode($response['body']);
+
+                if (isset($return->access_token)) {
+                    $expiredDate = time() + $return->expires_in;
+
+                    $this->wpcPlugin
+                        ->setTokenData(['access_token' => $return->access_token, 'expire' => $expiredDate])
+                        ->persist();
+
+                    return $return->access_token;
+                }
+            }
+            throw new \Exception('Something gone wrong with the Token API response.', 1);
+        } catch (\Exception $e) {
+            // log, display, etc
+            throw new \Exception($e->getMessage(),$e->getCode());
+        }
+    }
+
+    /**
+     * Retrieve CaptchEtat HTML 
+     * 
+     * @param array $urlParams Get Params for the API
+     * 
+     * @return array
+     */
+    public function getCaptchaElement($urlParams = []): array
+    {
+        $additionalOptions = [
+            'headers' => [
+                'Authorization' => 'Bearer ' . $this->getToken(),
+            ],
+        ];
+
+        
+        unset($urlParams['action']);
+        $urlParamsStr = http_build_query($urlParams);
+        try {
+            $response = $this->_call($this->wpcPlugin->getApiURL() . '/simple-captcha-endpoint?' . $urlParamsStr, 'GET', $additionalOptions);
+
+            if (!empty($response)) {
+                return ['content-type' => $response['headers']['Content-Type'], 'content' => $response['body']];
+            }
+        } catch (\Exception $e) {
+            // log, display, etc
+            return ["content" => "<div> erreur, code : " . $e->getCode() . " , message : " .$e->getMessage() . "</div>"];
+        }
+    }
+
+    /**
+     * Capcha validation
+     * 
+     * @param string $input 
+     * 
+     * @param string $id $capcha id
+     * 
+     * @return bool
+     * 
+     */
+    public function validateCaptcha($code, $id): bool
+    {
+        $return = false;
+
+        if (!empty($code) && !empty($id)) {
+            $additionalOptions = [
+                'headers' => [
+                    'Authorization' => 'Bearer ' . $this->getToken(),
+                    'Content-Type' => 'application/json'
+                ],
+                'body' => json_encode(['code' => $code, 'id' => $id])
+            ];
+
+            try {
+                $response = $this->_call($this->wpcPlugin->getApiURL()  . '/valider-captcha', 'POST', $additionalOptions);
+
+                $return = $response['body'] == 'false' ? false : true;
+            } catch (\Exception $e) {
+                // log, display, etc
+                throw new \Exception('validate captcha error '. $e->getCode() . " : " . $e->getMessage(), $e->getCode());
+            }
+        }
+
+        return $return;
+    }
+}
diff --git a/app/wpcPlugin.class.php b/app/wpcPlugin.class.php
new file mode 100644
index 0000000..c9c2ea1
--- /dev/null
+++ b/app/wpcPlugin.class.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace CaptchEtat;
+
+if (!defined('ABSPATH')) {
+    exit;
+}
+
+use CaptchEtat\Admin;
+use CaptchEtat\Service;
+use WP_Error;
+
+/**
+ * WordPress CaptchEtat Plugin Singleton class
+ * 
+ */
+class WpcPlugin
+{
+    /**
+     * WpcPlugin singleton instance
+     *
+     * @var WpcPlugin
+     */
+    static private $_instance;
+
+
+    /**
+     * Client ID
+     *
+     * @var string
+     */
+    private $_clientId = null;
+
+    /**
+     * Client Secret
+     *
+     * @var string
+     */
+    private $_clientSecret = null;
+
+    /**
+     * CaptchEtat API URL
+     *
+     * @var string
+     */
+    private $_apiUrl = null;
+
+
+    /**
+     * CaptchEtat Token generation URL (?)
+     *
+     * @var string|null
+     */
+    private $_tokenUrl = null;
+
+    /**
+     * CaptchEtat Token data expire, access_token
+     *
+     * @var array|null
+     */
+    private $_tokenData = null;
+
+    /**
+     * @var string|array
+     */
+    private $settings;
+
+    /**
+     * Array of forms with CaptchEtat or not
+     * @var array[bool]|array[]
+     */
+    private $_formsWithCaptchetat;
+
+
+    /**
+     * Registering singleton
+     *
+     * @return WpcPlugin
+     */
+    static public function register()
+    {
+        if (!isset(self::$_instance)) {
+            self::$_instance = new self();
+        }
+
+        return self::$_instance;
+    }
+
+    /**
+     * Private constructor
+     * 
+     */
+    protected function __construct()
+    {
+        $this->settings = get_option('glcapt_settings');
+        $this->hydrateWSettings();
+    }
+
+    /**
+     * Get client ID
+     *
+     * @return string|null
+     */
+    public function getClientId()
+    {
+        if ($this->_clientId === null) {
+            throw new \Exception("Missing configuration 'Client Id'  in extension configuration");
+        }
+        return $this->_clientId;
+    }
+
+    /**
+     * Get client Secret
+     *
+     * @return string|null
+     */
+    public function getClientSecret()
+    {
+        if ($this->_clientSecret === null) {
+            throw new \Exception("Missing configuration 'Client Secret'  in extension configuration");
+        }
+        return $this->_clientSecret;
+    }
+
+    /**
+     * Get API URL
+     *
+     * @return string|null
+     */
+    public function getApiURL()
+    {
+        if (!wp_http_validate_url($this->_apiUrl)) {
+            throw new \Exception('Bad or empty API URL : ' . $this->_apiUrl, 500);
+        }
+        return rtrim($this->_apiUrl, '/');
+    }
+
+    /**
+     * Get Token API URL
+     *
+     * @return string|null
+     */
+    public function getTokenURL()
+    {
+        if (!wp_http_validate_url($this->_tokenUrl)) {
+            throw new \Exception('Bad or empty Token URL : ' . $this->_tokenUrl, 500);
+        }
+        return rtrim($this->_tokenUrl, '/');
+    }
+
+    /**
+     * Get Token 
+     *
+     * @return string|null
+     */
+    public function getTokenData()
+    {
+        return $this->_tokenData;
+    }
+
+    /**
+     * Set Token data
+     * 
+     * @return self
+     */
+    public function setTokenData(array $tokenData)
+    {
+        $this->_tokenData = $tokenData;
+        $this->settings['glcapt_token_data'] = $tokenData;
+
+        return $this;
+    }
+
+    /**
+     * 
+     */
+    public function getFormsWithCaptchetat() {
+        return $this->_formsWithCaptchetat;
+    }
+
+    public function hydrateWSettings()
+    {
+        if (!empty($this->settings["glcapt_client_id"])) {
+            $this->_clientId =  esc_attr($this->settings["glcapt_client_id"]);
+        }
+
+        if (!empty($this->settings["glcapt_client_secret"])) {
+            $this->_clientSecret =  esc_attr($this->settings["glcapt_client_secret"]);
+        }
+
+        if (!empty($this->settings["glcapt_api_url"])) {
+            $this->_apiUrl =  esc_attr($this->settings["glcapt_api_url"]);
+        }
+
+        if (!empty($this->settings["glcapt_token_url"])) {
+            $this->_tokenUrl =  esc_attr($this->settings["glcapt_token_url"]);
+        }
+
+        if(!empty($this->settings["glcapt_forms"]) && is_array($this->settings["glcapt_forms"])) {
+            $this->_formsWithCaptchetat = $this->settings["glcapt_forms"];
+        }
+
+        if(!empty($this->settings["glcapt_token_data"]) && is_array($this->settings["glcapt_token_data"])) {
+            $this->_tokenData = $this->settings["glcapt_token_data"];
+        }
+    }
+
+    public function persist()
+    {
+        update_option('glcapt_settings', $this->settings);
+    }
+}
diff --git a/assets/scripts/gl-captchetat-form.js b/assets/scripts/gl-captchetat-form.js
new file mode 100644
index 0000000..9e1b4d0
--- /dev/null
+++ b/assets/scripts/gl-captchetat-form.js
@@ -0,0 +1,15 @@
+jQuery(document).ready(function () {
+    if (jQuery('div[id^="botdetect-captcha"').length > 0) {
+        var captcha =
+        jQuery('div[id^="botdetect-captcha"').captcha({
+            captchaEndpoint:  GLCAPTPARAMS.backendUrl
+        });
+        var glCaptForm = jQuery('div[id^="botdetect-captcha"').closest('form');
+
+       jQuery(glCaptForm).submit(function (event) {
+            // Le code entré par l’utilisateur récupéré en backend
+            jQuery('input[id^="captchaId"').val(captcha.getCaptchaId());
+        });
+    }
+});
+
diff --git a/assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js b/assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js
new file mode 100644
index 0000000..1159842
--- /dev/null
+++ b/assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js
@@ -0,0 +1,2 @@
+!function(a){"use strict";a.fn.captcha=function(b){function c(){return a.ajax({method:"GET",url:b.captchaEndpoint,data:{get:"html",c:p}})}function d(b,c){var d=j();a.ajax({method:"GET",url:d.validationUrl,data:{i:b},success:function(a){c(a)}})}function e(){var b=j();a("#"+b.options.userInputID).on("blur",function(){var c=a.trim(a(this).val());if(0!==c.length){var e=this;d(c,function(c){c||b.reloadImage(),a(e).trigger("validatecaptcha",[c])})}})}function f(b,c){return c=a.extend({dataType:"script",cache:!1,url:b},c||{}),a.ajax(c)}function g(){var c=a("#BDC_VCID_"+p).val();f(b.captchaEndpoint+"?get=script-include&c="+p+"&t="+c+"&cs=2").done(function(){setTimeout(i,200)})}function h(){var b=j();return void 0!==a("#"+b.options.userInputID).attr("data-correct-captcha")}function i(){h()&&e()}function j(){return void 0!==window.botdetect?window.botdetect.getInstanceByStyleName(p):null}function k(){var a=b.captchaEndpoint.split("/");return a[a.length-1]}function l(a){var c=b.captchaEndpoint.lastIndexOf(a);return b.captchaEndpoint.substring(0,c)}function m(a){var b=k(),c=l(b);a=a.replace(/<script.*<\/script>/g,"");for(var d,e,f,g=a.match(/(src|href)=\"([^"]+)\"/g),h=a,i=0;i<g.length;i++)d=g[i].slice(0,-1).replace(/src=\"|href=\"/,""),e=new RegExp(".*"+b),f=d.replace(e,c+b),h=h.replace(d,f);return h}function n(){c(b.captchaEndpoint,p).done(function(a){a=m(a),o.html(a),g()})}var o=this;if(0===o.length)throw new Error("Captcha html element cound not be found.");if(!b||!b.captchaEndpoint)throw new Error("The captchaEndpoint setting is required.");b.captchaEndpoint=b.captchaEndpoint.replace(/\/+$/g,"");var p=function(){var a;if(a=o.data("captchastylename"))return a;if(a=o.data("stylename"))return a;throw new Error("The data-captchastylename attribute is not found or its value is not set.")}();return o.init=function(){return n(),o},o.getCaptchaId=function(){return j().captchaId},o.getCaptchaCode=function(){return j().userInput.value},o.getUserEnteredCaptchaCode=function(){return o.getCaptchaCode()},o.reloadImage=function(){j().reloadImage()},o.validateUnsafe=function(b){var c=j();d(a.trim(a("#"+c.options.userInputID).val()),function(a){b(a),h()||a||c.reloadImage()})},o.init()}}(jQuery);
+//# sourceMappingURL=jquery-captcha.min.js.map
\ No newline at end of file
diff --git a/assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js.map b/assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js.map
new file mode 100644
index 0000000..b73f4a1
--- /dev/null
+++ b/assets/scripts/vendor/jquery-capcha/jquery-captcha.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/jquery-captcha.js"],"names":["$","fn","captcha","settings","_getHtml","ajax","method","url","captchaEndpoint","data","get","c","captchaStyleName","_validateUnsafe","captchaCode","onSuccess","instance","_getInstance","validationUrl","i","success","isCorrect","_registerUserInputBlurValidation","options","userInputID","on","trim","this","val","length","reloadImage","trigger","_getScript","extend","dataType","cache","_loadScriptIncludes","captchaId","done","setTimeout","_onLoadScriptsSuccess","_useUserInputBlurValidation","undefined","attr","window","botdetect","getInstanceByStyleName","_getCaptchaEndpointHandler","splited","split","_getBackendBaseUrl","captchaEndpointHandler","lastIndex","lastIndexOf","substring","_changeRelativeToAbsoluteUrls","originCaptchaHtml","backendUrl","replace","relativeUrl","relativeUrlPrefixPattern","absoluteUrl","relativeUrls","match","changedCaptchaHtml","slice","RegExp","_displayHtml","captchaHtml","element","html","Error","styleName","init","getCaptchaId","getCaptchaCode","userInput","value","getUserEnteredCaptchaCode","validateUnsafe","callback","isHuman","jQuery"],"mappings":"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","file":"jquery-captcha.min.js","sourcesContent":["(function($) {\r\n  'use strict';\r\n  \r\n  $.fn.captcha = function(settings) {\r\n    \r\n    var element = this;\r\n    \r\n    if (element.length === 0) {\r\n      throw new Error('Captcha html element cound not be found.');\r\n    }\r\n\r\n    if (!settings || !settings.captchaEndpoint) {\r\n      throw new Error('The captchaEndpoint setting is required.');\r\n    }\r\n    \r\n    // normalize captcha endpoint path\r\n    settings.captchaEndpoint = settings.captchaEndpoint.replace(/\\/+$/g, '');\r\n    \r\n    var captchaStyleName = _getCaptchaStyleName();\r\n  \r\n    // get captcha style name\r\n    function _getCaptchaStyleName() {\r\n      var styleName;\r\n\r\n      styleName = element.data('captchastylename');\r\n      if (styleName) {\r\n        return styleName;\r\n      }\r\n\r\n      // backward compatibility\r\n      styleName = element.data('stylename');\r\n      if (styleName) {\r\n        return styleName;\r\n      }\r\n\r\n      throw new Error('The data-captchastylename attribute is not found or its value is not set.');\r\n    };\r\n    \r\n    // get captcha html markup\r\n    function _getHtml() {\r\n      return $.ajax({\r\n        method: 'GET',\r\n        url: settings.captchaEndpoint,\r\n        data: {\r\n          get: 'html',\r\n          c: captchaStyleName\r\n        }\r\n      });\r\n    };\r\n\r\n    // ajax validate captcha\r\n    function _validateUnsafe(captchaCode, onSuccess) {\r\n      var instance = _getInstance();\r\n\r\n      $.ajax({\r\n        method: 'GET',\r\n        url: instance.validationUrl,\r\n        data: {\r\n          i: captchaCode\r\n        },\r\n        success: function (isCorrect) {\r\n          onSuccess(isCorrect);\r\n        }\r\n      });\r\n    };\r\n    \r\n    // ajax validate captcha on blur event and trigging the \r\n    // custom 'validatecaptcha' event to fire the validation result\r\n    function _registerUserInputBlurValidation() {\r\n      var instance = _getInstance();\r\n\r\n      $('#' + instance.options.userInputID).on('blur', function() {\r\n        var captchaCode = $.trim($(this).val());\r\n        if (captchaCode.length === 0) { return; }\r\n\r\n        var userInputID = this;\r\n        _validateUnsafe(captchaCode, function(isCorrect) {\r\n          if (!isCorrect) {\r\n            instance.reloadImage();\r\n          }\r\n          $(userInputID).trigger('validatecaptcha', [isCorrect]);\r\n        });\r\n      });\r\n    };\r\n    \r\n    // a custom of $.getScript(), which lets changing the options\r\n    function _getScript(url, options) {\r\n      options = $.extend({\r\n        dataType: 'script',\r\n        cache: false,\r\n        url: url\r\n      }, options || {});\r\n      return $.ajax(options);\r\n    };\r\n    \r\n    // load botdetect scripts and execute them\r\n    function _loadScriptIncludes() {\r\n      var captchaId = $('#BDC_VCID_' + captchaStyleName).val();\r\n      var scriptIncludeUrl = settings.captchaEndpoint + '?get=script-include&c=' + captchaStyleName + '&t=' + captchaId + '&cs=2';\r\n      _getScript(scriptIncludeUrl).done(function() {\r\n        setTimeout(_onLoadScriptsSuccess, 200);\r\n      });\r\n    };\r\n    \r\n    // use user input blur validation if the input element has data-correct-captcha attribute\r\n    function _useUserInputBlurValidation() {\r\n      var instance = _getInstance();\r\n      return ($('#' + instance.options.userInputID).attr('data-correct-captcha') !== undefined);\r\n    };\r\n    \r\n    // fire the custom event when botdetect scripts are loaded\r\n    function _onLoadScriptsSuccess() {\r\n      if (_useUserInputBlurValidation()) {\r\n        _registerUserInputBlurValidation();\r\n      }\r\n    }\r\n    \r\n    // get botdetect captcha client-side instance\r\n    function _getInstance() {\r\n      var instance = null;\r\n      if (typeof window.botdetect !== 'undefined') {\r\n        return window.botdetect.getInstanceByStyleName(captchaStyleName);\r\n      }\r\n      return instance;\r\n    };\r\n\r\n    // get captcha endpoint handler from configued captchaEndpoint value,\r\n    // the result can be \"simple-captcha-endpoint.ashx\", \"botdetectcaptcha\",\r\n    // or \"simple-botdetect.php\"\r\n    function _getCaptchaEndpointHandler() {\r\n      var splited = settings.captchaEndpoint.split('/');\r\n      return splited[splited.length - 1];\r\n    };\r\n\r\n    // get backend base url from configued captchaEndpoint value\r\n    function _getBackendBaseUrl(captchaEndpointHandler) {\r\n      var lastIndex = settings.captchaEndpoint.lastIndexOf(captchaEndpointHandler);\r\n      return settings.captchaEndpoint.substring(0, lastIndex);\r\n    };\r\n\r\n    // change relative to absolute urls in captcha html markup\r\n    function _changeRelativeToAbsoluteUrls(originCaptchaHtml) {\r\n      var captchaEndpointHandler = _getCaptchaEndpointHandler();\r\n      var backendUrl = _getBackendBaseUrl(captchaEndpointHandler);\r\n\r\n      originCaptchaHtml = originCaptchaHtml.replace(/<script.*<\\/script>/g, '');\r\n      var relativeUrls = originCaptchaHtml.match(/(src|href)=\\\"([^\"]+)\\\"/g);\r\n      \r\n      var relativeUrl, relativeUrlPrefixPattern, absoluteUrl,\r\n          changedCaptchaHtml = originCaptchaHtml;\r\n\r\n      for (var i = 0; i < relativeUrls.length; i++) {\r\n        relativeUrl = relativeUrls[i].slice(0, -1).replace(/src=\\\"|href=\\\"/, '');\r\n        relativeUrlPrefixPattern = new RegExp(\".*\" + captchaEndpointHandler);\r\n        absoluteUrl = relativeUrl.replace(relativeUrlPrefixPattern, backendUrl + captchaEndpointHandler);\r\n        changedCaptchaHtml = changedCaptchaHtml.replace(relativeUrl, absoluteUrl);\r\n      }\r\n\r\n      return changedCaptchaHtml;\r\n    };\r\n    \r\n    // display captcha html markup in view\r\n    function _displayHtml() {\r\n      _getHtml(settings.captchaEndpoint, captchaStyleName).done(function(captchaHtml) {\r\n        captchaHtml = _changeRelativeToAbsoluteUrls(captchaHtml) ;\r\n        element.html(captchaHtml);\r\n        _loadScriptIncludes();\r\n      });\r\n    }\r\n    \r\n    // plugin initialization - we display the captcha html markup in view\r\n    element.init = function() {\r\n      _displayHtml();\r\n      return element;\r\n    };\r\n    \r\n    // captcha id for validating captcha at server-side\r\n    element.getCaptchaId = function() {\r\n      var instance = _getInstance();\r\n      return instance.captchaId;\r\n    };\r\n\r\n    // get the user entered captcha code\r\n    // keep this function for backward compatibility\r\n    element.getCaptchaCode = function() {\r\n      var instance = _getInstance();\r\n      return instance.userInput.value;\r\n    };\r\n\r\n    element.getUserEnteredCaptchaCode = function() {\r\n      return element.getCaptchaCode();\r\n    };\r\n    \r\n    // reload new captcha image\r\n    element.reloadImage = function() {\r\n      var instance = _getInstance();\r\n      instance.reloadImage();\r\n    };\r\n\r\n    // validate captcha on client-side and execute user callback function on ajax success\r\n    element.validateUnsafe = function(callback) {\r\n      var instance = _getInstance();\r\n      var captchaCode = $.trim($('#' + instance.options.userInputID).val());\r\n      _validateUnsafe(captchaCode, function(isHuman) {\r\n        callback(isHuman);\r\n        if (!_useUserInputBlurValidation() && !isHuman) {\r\n          instance.reloadImage();\r\n        }\r\n      });\r\n    };\r\n\r\n    return element.init();\r\n  };\r\n  \r\n}(jQuery));\r\n"]}
\ No newline at end of file
diff --git a/assets/styles/acf.css b/assets/styles/acf.css
new file mode 100644
index 0000000..02f00ed
--- /dev/null
+++ b/assets/styles/acf.css
@@ -0,0 +1,9 @@
+.acf-field input[type=text].captchetat-acf {
+    width:250px;
+    margin-top:1em;
+    text-transform: uppercase;
+}
+
+input.captchetat-acf::placeholder {
+    opacity:0.8;
+}
\ No newline at end of file
diff --git a/assets/styles/styles.css b/assets/styles/styles.css
new file mode 100644
index 0000000..1996b10
--- /dev/null
+++ b/assets/styles/styles.css
@@ -0,0 +1,11 @@
+.captchetat-input {
+    width: 250px;
+    margin-top: 1rem;
+    outline: none;
+}
+
+.captchetat-input::placeholder {
+    opacity: 0.8;
+    font-size: 1rem;
+    vertical-align: text-top;
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..01f77cf
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,13 @@
+{
+  "name": "grandlyon/wp-captchetat",
+  "type": "wordpress-plugin",
+  "licence": "GPL-3.0+",
+  "keywords": [
+    "wordpress",
+    "captcha",
+    "captchetat"
+  ],
+  "require": {
+    "php":">=5.6.0"
+  }
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..0e76c7b
--- /dev/null
+++ b/index.php
@@ -0,0 +1,2 @@
+<?php
+/** nothing here */
\ No newline at end of file
diff --git a/languages/wp-capchetat.pot b/languages/wp-capchetat.pot
new file mode 100644
index 0000000..e69de29
diff --git a/wp-captchetat.php b/wp-captchetat.php
new file mode 100644
index 0000000..313f698
--- /dev/null
+++ b/wp-captchetat.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * CAPTCHETAT for WordPress by Metropole de Lyon
+ * 
+ * @copyright Copyright (C) 2023 - Métropole de Lyon
+ * @license   ?
+ *
+ * @wordpress-plugin
+ * Plugin Name: CAPTCHETAT for WordPress by Metropole de Lyon
+ * Version:     0.1
+ * Description: Implementation of the CaptchEtat API for WordPress (see https://api.gouv.fr/les-api/api-captchetat)
+ * Author: Yohan RICCI, Matthieu BENOIST, AUSY
+ * Author URI:  
+ * License:  ?
+ * Requires at least: 5.2
+ * Requires PHP: 7.0
+ *
+ * WC requires at least: 5.2
+ * WC tested up to: 6.2
+ */
+
+ namespace CaptchEtat;
+
+use CaptchEtat\Admin\CaptchEtatAdmin;
+use CaptchEtat\Forms\CaptchEtatForms;
+use CaptchEtat\AcfField\GLCaptchEtatField;
+
+ // definition
+
+ if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+
+if (!defined('WP_CAPTCHETAT_VERSION')) {
+    define ('WP_CAPTCHETAT_VERSION', '0?1');
+}
+
+if (!defined('WP_CAPTCHETAT_PATH')) {
+    define ('WP_CAPTCHETAT_PATH', plugin_dir_path(__FILE__));
+}
+
+if (!defined('WP_CAPTCHETAT_PLUGIN_URL')) {
+    define ('WP_CAPTCHETAT_PLUGIN_URL', plugin_dir_url(__FILE__));
+}
+
+require WP_CAPTCHETAT_PATH . 'app/wpcPlugin.class.php';
+require WP_CAPTCHETAT_PATH . 'app/service/CaptchEtatService.class.php';
+require WP_CAPTCHETAT_PATH . 'app/admin/CaptchEtatAdmin.class.php';
+require WP_CAPTCHETAT_PATH . 'app/forms/CaptchEtatForms.class.php';
+
+
+if (function_exists('acf_register_field_type') ) {
+    require WP_CAPTCHETAT_PATH . 'app/acfField/GLCaptchEtatField.class.php';
+    add_action('init', function(){
+        acf_register_field_type(GLCaptchEtatField::class);
+    });
+}
+
+if (class_exists('WpcPlugin') || true) {
+    //$plugin = WpcPlugin::register();
+    //admin page
+    $admin = CaptchEtatAdmin::register();
+    //forms action for displayin and validating captchas with the capchetat library 
+    $forms = CaptchEtatForms::register();
+}
+
+
+
+
+
-- 
GitLab