diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4ae70bc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.DS_Store
+Thumbs.db
+*.log
+.idea/
+.vscode/
+*.swp
+*~
+/vendor/
+node_modules/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6da2870
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# TON Italia Registration
+
+Plugin WordPress per la domanda di iscrizione socio **TON ITALIA ODV**: form shortcode, GDPR, CAPTCHA, gestione ammissioni e documenti.
+
+## Requisiti
+
+- WordPress 6.0+
+- PHP 8.0+
+
+## Installazione
+
+1. Copia la cartella `ton-italia-registration` in `wp-content/plugins/`
+2. Attiva il plugin da **Plugin → Plugin installati**
+3. Inserisci lo shortcode `[ton_registration_form]` in una pagina
+4. Configura testi e email in **TON Iscrizioni → Impostazioni**
+
+## Repository
+
+- GitBucket: `https://niphredil.duckdns.org/git/fabio/tonitalia-pluginsoci.git`
+
+## Documentazione
+
+Vedi `readme.txt` per changelog e note GDPR (formato WordPress.org).
+
+## Versione
+
+Vedi intestazione in `ton-italia-registration.php` (`TON_REG_VERSION`).
diff --git a/admin/class-admin.php b/admin/class-admin.php
new file mode 100644
index 0000000..d97a111
--- /dev/null
+++ b/admin/class-admin.php
@@ -0,0 +1,648 @@
+ 'string',
+ 'sanitize_callback' => $sanitize,
+ )
+ );
+ }
+ }
+
+ /**
+ * @param mixed $value Value.
+ * @return string
+ */
+ public static function sanitize_setting( $value ) {
+ if ( is_array( $value ) ) {
+ return '';
+ }
+ return wp_kses_post( wp_unslash( (string) $value ) );
+ }
+
+ /**
+ * @param string $value Checkbox value.
+ * @return string
+ */
+ public static function sanitize_checkbox( $value ) {
+ return '1' === (string) $value ? '1' : '0';
+ }
+
+ /**
+ * List page.
+ */
+ public static function page_list() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ if ( isset( $_GET['action'], $_GET['id'] ) && 'view' === $_GET['action'] ) {
+ self::page_detail( (int) $_GET['id'] );
+ return;
+ }
+
+ $table = new TON_Reg_Registration_List_Table();
+ $table->prepare_items();
+
+ $status = isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '';
+ ?>
+
+
+
+
+
+
+
+ display(); ?>
+
+ ' . esc_html__( 'Non trovato.', 'ton-italia-registration' ) . '
';
+ return;
+ }
+ ?>
+
+
+
←
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ anonymized_at ) : ?>
+
+
+
+
+
+
+
+
+
+ ' . esc_html( $text ) . '';
+ }
+
+ /**
+ * @param string $datetime UTC datetime from DB.
+ * @return string
+ */
+ private static function format_consent_datetime( $datetime ) {
+ $ts = strtotime( $datetime . ' UTC' );
+ if ( ! $ts ) {
+ return $datetime;
+ }
+ return sprintf(
+ /* translators: %s: formatted date and time */
+ __( 'Registrato il %s', 'ton-italia-registration' ),
+ wp_date( 'd/m/Y H:i', $ts )
+ );
+ }
+
+ /**
+ * @param string $date Date Y-m-d.
+ * @return string
+ */
+ private static function format_date_display( $date ) {
+ $ts = strtotime( $date );
+ return $ts ? wp_date( 'd/m/Y', $ts ) : $date;
+ }
+
+ /**
+ * Admin notices for document upload / errors.
+ */
+ private static function render_admin_notices() {
+ if ( isset( $_GET['doc_ok'] ) ) {
+ echo '' . esc_html__( 'Documento caricato.', 'ton-italia-registration' ) . '
';
+ }
+ if ( isset( $_GET['doc_deleted'] ) ) {
+ echo '' . esc_html__( 'Documento eliminato.', 'ton-italia-registration' ) . '
';
+ }
+ if ( ! empty( $_GET['doc_error'] ) ) {
+ $msg = sanitize_text_field( wp_unslash( rawurldecode( $_GET['doc_error'] ) ) );
+ echo '';
+ }
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @param object $row Registration row.
+ * @param string $type Document type.
+ */
+ private static function render_document_block( $id, $row, $type ) {
+ $column = TON_Reg_Documents::get_column( $type );
+ $file = $column ? TON_Reg_Documents::get_file_info( (int) ( $row->$column ?? 0 ) ) : null;
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ />
+
+
+
+ rawurlencode( $result->get_error_message() ) ),
+ $redirect
+ )
+ );
+ exit;
+ }
+
+ wp_safe_redirect( add_query_arg( 'doc_ok', '1', $redirect ) );
+ exit;
+ }
+
+ /**
+ * Delete member document.
+ */
+ public static function delete_document() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ $id = isset( $_GET['registration_id'] ) ? (int) $_GET['registration_id'] : 0;
+ $type = isset( $_GET['document_type'] ) ? sanitize_text_field( wp_unslash( $_GET['document_type'] ) ) : '';
+
+ check_admin_referer( 'ton_reg_del_doc_' . $id . '_' . $type );
+
+ TON_Reg_Documents::delete_document( $id, $type );
+
+ wp_safe_redirect(
+ add_query_arg(
+ 'doc_deleted',
+ '1',
+ admin_url( 'admin.php?page=ton-registrations&action=view&id=' . $id )
+ )
+ );
+ exit;
+ }
+
+ /**
+ * Settings page.
+ */
+ public static function page_settings() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+ ?>
+
+
+
+
+
+
+
+
+
[ton_registration_form]
+
+
+ [ton_registration_form class="mia-classe"]
+
+
+
+
+ __( 'Istruzioni bonifico', 'ton-italia-registration' ),
+ 'ton_reg_statuto_declaration' => __( 'Dichiarazione statuto (checkbox 1)', 'ton-italia-registration' ),
+ 'ton_reg_privacy_notice' => __( 'Informativa privacy', 'ton-italia-registration' ),
+ 'ton_reg_newsletter_label' => __( 'Label consenso newsletter', 'ton-italia-registration' ),
+ 'ton_reg_minor_notice' => __( 'Avviso minorenni', 'ton-italia-registration' ),
+ 'ton_reg_success_message' => __( 'Messaggio successo', 'ton-italia-registration' ),
+ 'ton_reg_email_admin_body' => __( 'Email admin (testo)', 'ton-italia-registration' ),
+ 'ton_reg_email_user_body' => __( 'Email utente (testo)', 'ton-italia-registration' ),
+ );
+ ?>
+
+
+
+
+
+ sanitize_text_field( wp_unslash( $_POST['status'] ?? 'pending' ) ),
+ 'libro_associati_date' => sanitize_text_field( wp_unslash( $_POST['libro_associati_date'] ?? '' ) ) ?: null,
+ 'admin_notes' => sanitize_textarea_field( wp_unslash( $_POST['admin_notes'] ?? '' ) ),
+ )
+ );
+
+ wp_safe_redirect( admin_url( 'admin.php?page=ton-registrations&action=view&id=' . $id . '&updated=1' ) );
+ exit;
+ }
+
+ /**
+ * GDPR export download.
+ */
+ public static function gdpr_export() {
+ $id = isset( $_GET['id'] ) ? (int) $_GET['id'] : 0;
+ if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'ton_reg_gdpr_' . $id ) ) {
+ wp_die( esc_html__( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ $data = TON_Reg_Gdpr::export( $id );
+ if ( ! $data ) {
+ wp_die( esc_html__( 'Non trovato.', 'ton-italia-registration' ) );
+ }
+
+ $format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'json';
+
+ if ( 'csv' === $format ) {
+ header( 'Content-Type: text/csv; charset=utf-8' );
+ header( 'Content-Disposition: attachment; filename=ton-registration-' . $id . '.csv' );
+ $out = fopen( 'php://output', 'w' );
+ foreach ( $data as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $value = wp_json_encode( $value, JSON_UNESCAPED_UNICODE );
+ }
+ fputcsv( $out, array( (string) $key, (string) $value ) );
+ }
+ fclose( $out );
+ exit;
+ }
+
+ header( 'Content-Type: application/json; charset=utf-8' );
+ header( 'Content-Disposition: attachment; filename=ton-registration-' . $id . '.json' );
+ echo wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
+ exit;
+ }
+
+ /**
+ * GDPR anonymize.
+ */
+ public static function gdpr_anonymize() {
+ $id = isset( $_GET['id'] ) ? (int) $_GET['id'] : 0;
+ if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'ton_reg_gdpr_' . $id ) ) {
+ wp_die( esc_html__( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ TON_Reg_Gdpr::anonymize( $id, 'admin action' );
+ wp_safe_redirect( admin_url( 'admin.php?page=ton-registrations&action=view&id=' . $id . '&updated=1' ) );
+ exit;
+ }
+
+ /**
+ * GDPR erase.
+ */
+ public static function gdpr_erase() {
+ $id = isset( $_GET['id'] ) ? (int) $_GET['id'] : 0;
+ if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'ton_reg_gdpr_' . $id ) ) {
+ wp_die( esc_html__( 'Non autorizzato.', 'ton-italia-registration' ) );
+ }
+
+ TON_Reg_Gdpr::erase( $id, 'admin action' );
+ wp_safe_redirect( admin_url( 'admin.php?page=ton-registrations&updated=1' ) );
+ exit;
+ }
+}
diff --git a/admin/class-registration-list-table.php b/admin/class-registration-list-table.php
new file mode 100644
index 0000000..cebd5df
--- /dev/null
+++ b/admin/class-registration-list-table.php
@@ -0,0 +1,149 @@
+ 'registration',
+ 'plural' => 'registrations',
+ 'ajax' => false,
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function get_columns() {
+ return array(
+ 'id' => __( 'ID', 'ton-italia-registration' ),
+ 'cognome' => __( 'Cognome', 'ton-italia-registration' ),
+ 'nome' => __( 'Nome', 'ton-italia-registration' ),
+ 'email' => __( 'Email', 'ton-italia-registration' ),
+ 'status' => __( 'Stato', 'ton-italia-registration' ),
+ 'created_at' => __( 'Data', 'ton-italia-registration' ),
+ );
+ }
+
+ /**
+ * @param object $item Item.
+ * @return string
+ */
+ public function column_id( $item ) {
+ $view_url = admin_url( 'admin.php?page=ton-registrations&action=view&id=' . (int) $item->id );
+ $actions = array(
+ 'view' => '' . esc_html__( 'Dettaglio', 'ton-italia-registration' ) . ' ',
+ );
+ return (int) $item->id . $this->row_actions( $actions );
+ }
+
+ /**
+ * @param object $item Item.
+ * @return string
+ */
+ public function column_status( $item ) {
+ $labels = array(
+ 'pending' => __( 'In attesa', 'ton-italia-registration' ),
+ 'admitted' => __( 'Ammesso', 'ton-italia-registration' ),
+ 'rejected' => __( 'Non ammesso', 'ton-italia-registration' ),
+ );
+ $label = $labels[ $item->status ] ?? $item->status;
+ if ( $item->anonymized_at ) {
+ $label .= ' (' . __( 'anonimizzato', 'ton-italia-registration' ) . ')';
+ }
+ return esc_html( $label );
+ }
+
+ /**
+ * @param object $item Item.
+ * @param string $column_name Column.
+ * @return string
+ */
+ public function column_default( $item, $column_name ) {
+ return esc_html( $item->$column_name ?? '' );
+ }
+
+ /**
+ * @return string
+ */
+ protected function get_primary_column_name() {
+ return 'cognome';
+ }
+
+ /**
+ * @return array
+ */
+ protected function get_sortable_columns() {
+ return array(
+ 'cognome' => array( 'cognome', false ),
+ 'created_at' => array( 'created_at', true ),
+ 'status' => array( 'status', false ),
+ );
+ }
+
+ /**
+ * Prepare items.
+ */
+ public function prepare_items() {
+ $per_page = 20;
+ $status = isset( $_REQUEST['status'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['status'] ) ) : '';
+ $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
+
+ $total = TON_Reg_Database::count(
+ array(
+ 'status' => $status,
+ 'search' => $search,
+ )
+ );
+
+ $total_pages = max( 1, (int) ceil( $total / $per_page ) );
+ $page = max( 1, min( $this->get_pagenum(), $total_pages ) );
+
+ $this->items = TON_Reg_Database::query(
+ array(
+ 'status' => $status,
+ 'search' => $search,
+ 'limit' => $per_page,
+ 'offset' => ( $page - 1 ) * $per_page,
+ )
+ );
+
+ $this->set_pagination_args(
+ array(
+ 'total_items' => $total,
+ 'per_page' => $per_page,
+ 'total_pages' => $total_pages,
+ )
+ );
+
+ $columns = $this->get_columns();
+ $hidden = array();
+ $sortable = $this->get_sortable_columns();
+ $this->_column_headers = array( $columns, $hidden, $sortable );
+ }
+
+ /**
+ * @return string
+ */
+ public function no_items() {
+ esc_html_e( 'Nessuna iscrizione trovata.', 'ton-italia-registration' );
+ }
+}
diff --git a/assets/css/admin.css b/assets/css/admin.css
new file mode 100644
index 0000000..c81a577
--- /dev/null
+++ b/assets/css/admin.css
@@ -0,0 +1,90 @@
+.ton-reg-shortcode-box {
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ border-left: 4px solid #2d6a4f;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
+ margin: 1rem 0 1.5rem;
+ max-width: 52rem;
+ padding: 1rem 1.25rem 1.25rem;
+}
+
+.ton-reg-shortcode-box__title {
+ font-size: 1.1em;
+ margin: 0 0 0.5rem;
+}
+
+.ton-reg-shortcode-box__text {
+ margin: 0 0 1rem;
+}
+
+.ton-reg-shortcode-box__code {
+ margin: 0 0 0.5rem;
+}
+
+.ton-reg-shortcode-box__code code {
+ display: inline-block;
+ font-size: 14px;
+ padding: 0.35rem 0.6rem;
+}
+
+.ton-reg-shortcode-box__hint {
+ margin: 0.75rem 0 0;
+}
+
+.ton-reg-shortcode-box__hint code {
+ font-size: 13px;
+}
+
+.ton-reg-doc-block {
+ background: #f6f7f7;
+ border: 1px solid #dcdcde;
+ border-radius: 4px;
+ margin: 0 0 1rem;
+ max-width: 40rem;
+ padding: 1rem 1.25rem;
+}
+
+.ton-reg-doc-block h3 {
+ margin: 0 0 0.5rem;
+}
+
+.ton-reg-doc-block__file {
+ margin: 0 0 0.75rem;
+}
+
+.ton-reg-doc-block__upload {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.ton-reg-doc-block__upload input[type="file"] {
+ max-width: 100%;
+}
+
+.ton-reg-consents th {
+ width: 220px;
+}
+
+.ton-reg-consent {
+ display: inline-block;
+ font-weight: 600;
+ padding: 0.2rem 0.5rem;
+ border-radius: 3px;
+}
+
+.ton-reg-consent--yes {
+ background: #d8f3dc;
+ color: #1b4332;
+}
+
+.ton-reg-consent--no {
+ background: #ffe5e5;
+ color: #9b2226;
+}
+
+.ton-reg-consent--neutral {
+ color: #646970;
+ font-weight: 400;
+}
diff --git a/assets/css/form.css b/assets/css/form.css
new file mode 100644
index 0000000..4c7954a
--- /dev/null
+++ b/assets/css/form.css
@@ -0,0 +1,178 @@
+.ton-reg-form {
+ max-width: 42rem;
+ margin: 0 auto 2rem;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.ton-reg-form__intro {
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+}
+
+.ton-reg-form__fieldset {
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ margin: 0 0 1.5rem;
+ padding: 1rem 1.25rem;
+}
+
+.ton-reg-form__fieldset legend {
+ font-weight: 600;
+ padding: 0 0.25rem;
+}
+
+.ton-reg-form__grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem 1rem;
+}
+
+.ton-reg-form__grid--narrow {
+ max-width: 24rem;
+}
+
+@media (max-width: 600px) {
+ .ton-reg-form__grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.ton-reg-form__field {
+ margin: 0;
+}
+
+.ton-reg-form__field label {
+ display: block;
+ font-weight: 500;
+ margin-bottom: 0.25rem;
+}
+
+.ton-reg-form__input {
+ width: 100%;
+ padding: 0.5rem 0.65rem;
+ border: 1px solid #888;
+ border-radius: 3px;
+ box-sizing: border-box;
+ color: #1a1a1a;
+ background: #fff;
+}
+
+.ton-reg-form__input::placeholder {
+ color: #9ca3af;
+ opacity: 1;
+}
+
+.ton-reg-form__input:placeholder-shown {
+ color: #9ca3af;
+}
+
+.ton-reg-form__input:not(:placeholder-shown) {
+ color: #1a1a1a;
+}
+
+.ton-reg-form__input:focus {
+ border-color: #2d6a4f;
+ color: #1a1a1a;
+ outline: 2px solid rgba(45, 106, 79, 0.25);
+ outline-offset: 1px;
+}
+
+.ton-reg-form__input:invalid:not(:focus):not(:placeholder-shown),
+.ton-reg-form__field--invalid .ton-reg-form__input {
+ border-color: #b00020;
+}
+
+.ton-reg-form__error {
+ color: #b00020;
+ display: block;
+ font-size: 0.85rem;
+ margin-top: 0.25rem;
+}
+
+.ton-reg-form__error[hidden] {
+ display: none;
+}
+
+.ton-reg-form__checkbox {
+ display: flex;
+ gap: 0.5rem;
+ align-items: flex-start;
+ margin: 0.75rem 0;
+}
+
+.ton-reg-form__checkbox input {
+ margin-top: 0.2rem;
+ flex-shrink: 0;
+}
+
+.ton-reg-form__checkbox--invalid {
+ outline: 2px solid #b00020;
+ outline-offset: 2px;
+ border-radius: 2px;
+}
+
+.ton-reg-form__declaration,
+.ton-reg-form__minor {
+ font-size: 0.95rem;
+}
+
+.ton-reg-form__minor {
+ font-style: italic;
+ color: #555;
+}
+
+.ton-reg-form__richtext ul {
+ margin: 0.5rem 0;
+ padding-left: 1.25rem;
+}
+
+.ton-reg-form__honeypot {
+ position: absolute;
+ left: -9999px;
+ height: 0;
+ overflow: hidden;
+}
+
+.ton-reg-form__button {
+ background: #2d6a4f;
+ color: #fff;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ font-size: 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.ton-reg-form__button:hover,
+.ton-reg-form__button:focus {
+ background: #1b4332;
+}
+
+.ton-reg-form--success,
+.ton-reg-form--error {
+ padding: 1rem 1.25rem;
+ border-radius: 4px;
+}
+
+.ton-reg-form--success {
+ background: #d8f3dc;
+ border: 1px solid #95d5b2;
+}
+
+.ton-reg-form--error {
+ background: #ffe5e5;
+ border: 1px solid #f4a4a4;
+}
+
+.ton-reg-form .required {
+ color: #b00020;
+}
+
+.ton-reg-form__warning {
+ background: #fff3cd;
+ border: 1px solid #ffc107;
+ padding: 0.75rem 1rem;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
diff --git a/assets/js/form.js b/assets/js/form.js
new file mode 100644
index 0000000..f99dff7
--- /dev/null
+++ b/assets/js/form.js
@@ -0,0 +1,235 @@
+(function () {
+ 'use strict';
+
+ var form = document.querySelector('.ton-reg-form form');
+ if (!form || typeof tonRegForm === 'undefined') {
+ return;
+ }
+
+ var cfRegex = /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/;
+ var dateRegex = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
+
+ function showFieldError(fieldWrap, message) {
+ var input = fieldWrap.querySelector('.ton-reg-form__input');
+ var error = fieldWrap.querySelector('.ton-reg-form__error');
+ if (!input || !error) {
+ return;
+ }
+ fieldWrap.classList.add('ton-reg-form__field--invalid');
+ error.textContent = message;
+ error.hidden = false;
+ input.setCustomValidity(message);
+ }
+
+ function clearFieldError(fieldWrap) {
+ var input = fieldWrap.querySelector('.ton-reg-form__input');
+ var error = fieldWrap.querySelector('.ton-reg-form__error');
+ if (!input || !error) {
+ return;
+ }
+ fieldWrap.classList.remove('ton-reg-form__field--invalid');
+ error.hidden = true;
+ error.textContent = '';
+ input.setCustomValidity('');
+ }
+
+ function clearAllErrors() {
+ form.querySelectorAll('.ton-reg-form__field').forEach(clearFieldError);
+ form.querySelectorAll('.ton-reg-form__checkbox--invalid').forEach(function (el) {
+ el.classList.remove('ton-reg-form__checkbox--invalid');
+ });
+ }
+
+ function getFieldWrap(name) {
+ return form.querySelector('.ton-reg-form__field[data-field="' + name + '"]');
+ }
+
+ function validateField(name) {
+ var wrap = getFieldWrap(name);
+ if (!wrap) {
+ return true;
+ }
+ var input = wrap.querySelector('.ton-reg-form__input');
+ if (!input) {
+ return true;
+ }
+
+ var value = (input.value || '').trim();
+
+ if (input.required && !value) {
+ showFieldError(wrap, tonRegForm.msgRequired);
+ return false;
+ }
+
+ if (!value) {
+ clearFieldError(wrap);
+ return true;
+ }
+
+ if ('email' === input.type && !input.checkValidity()) {
+ showFieldError(wrap, tonRegForm.msgEmail);
+ return false;
+ }
+
+ if ('codice_fiscale' === name) {
+ if (!cfRegex.test(value.toUpperCase())) {
+ showFieldError(wrap, tonRegForm.msgCf);
+ return false;
+ }
+ }
+
+ if ('cap' === name && !/^[0-9]{5}$/.test(value)) {
+ showFieldError(wrap, tonRegForm.msgCap);
+ return false;
+ }
+
+ if (('provincia_nascita' === name || 'provincia_residenza' === name) && !/^[A-Za-z]{2}$/.test(value)) {
+ showFieldError(wrap, tonRegForm.msgProvincia);
+ return false;
+ }
+
+ if (('data_nascita' === name || 'data_dichiarazione' === name) && !dateRegex.test(value)) {
+ showFieldError(wrap, tonRegForm.msgDate);
+ return false;
+ }
+
+ if (input.pattern && !new RegExp('^(?:' + input.pattern + ')$').test(value)) {
+ showFieldError(wrap, tonRegForm.msgRequired);
+ return false;
+ }
+
+ clearFieldError(wrap);
+ return true;
+ }
+
+ function validateCheckboxes() {
+ var ok = true;
+ form.querySelectorAll('input[type="checkbox"][required]').forEach(function (cb) {
+ var label = cb.closest('.ton-reg-form__checkbox');
+ if (!cb.checked) {
+ if (label) {
+ label.classList.add('ton-reg-form__checkbox--invalid');
+ }
+ ok = false;
+ } else if (label) {
+ label.classList.remove('ton-reg-form__checkbox--invalid');
+ }
+ });
+ return ok;
+ }
+
+ function validateCaptcha() {
+ var captcha = form.querySelector('#ton_reg_captcha_answer');
+ if (!captcha) {
+ return true;
+ }
+ if (!captcha.value && captcha.required) {
+ captcha.setCustomValidity(tonRegForm.msgCaptcha);
+ return false;
+ }
+ captcha.setCustomValidity('');
+ return true;
+ }
+
+ form.querySelectorAll('.ton-reg-form__input').forEach(function (input) {
+ input.addEventListener('input', function () {
+ var wrap = input.closest('.ton-reg-form__field');
+ if (wrap) {
+ clearFieldError(wrap);
+ }
+ if ('codice_fiscale' === input.name && input.value) {
+ input.value = input.value.toUpperCase().replace(/\s+/g, '');
+ }
+ if (('provincia_nascita' === input.name || 'provincia_residenza' === input.name) && input.value) {
+ input.value = input.value.toUpperCase();
+ }
+ });
+
+ input.addEventListener('blur', function () {
+ if (input.name) {
+ validateField(input.name);
+ }
+ });
+ });
+
+ var birthInput = form.querySelector('#ton_reg_data_nascita');
+ if (birthInput) {
+ birthInput.addEventListener('change', function () {
+ var warning = form.querySelector('.ton-reg-form__warning');
+ if (warning) {
+ warning.remove();
+ }
+ var date = birthInput.value;
+ if (!date || !dateRegex.test(date)) {
+ return;
+ }
+ var parts = date.split('-');
+ var birth = new Date(parseInt(parts[2], 10), parseInt(parts[1], 10) - 1, parseInt(parts[0], 10));
+ var today = new Date();
+ var age = today.getFullYear() - birth.getFullYear();
+ var m = today.getMonth() - birth.getMonth();
+ if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) {
+ age -= 1;
+ }
+ if (age < 18) {
+ var div = document.createElement('div');
+ div.className = 'ton-reg-form__warning';
+ div.setAttribute('role', 'alert');
+ div.textContent =
+ 'Se sei minorenne, la domanda deve essere firmata da entrambi i genitori o dal tutore legale.';
+ form.insertBefore(div, form.firstChild);
+ }
+ });
+ }
+
+ form.addEventListener('submit', function (e) {
+ clearAllErrors();
+
+ var fields = [
+ 'cognome',
+ 'nome',
+ 'luogo_nascita',
+ 'provincia_nascita',
+ 'data_nascita',
+ 'codice_fiscale',
+ 'comune_residenza',
+ 'provincia_residenza',
+ 'indirizzo',
+ 'numero_civico',
+ 'cap',
+ 'telefono',
+ 'email',
+ 'luogo_dichiarazione',
+ 'data_dichiarazione',
+ ];
+
+ var valid = true;
+ fields.forEach(function (name) {
+ if (!validateField(name)) {
+ valid = false;
+ }
+ });
+
+ if (!validateCheckboxes()) {
+ valid = false;
+ }
+
+ if (!validateCaptcha()) {
+ valid = false;
+ }
+
+ if (!valid) {
+ e.preventDefault();
+ var firstInvalid = form.querySelector('.ton-reg-form__field--invalid .ton-reg-form__input, .ton-reg-form__checkbox--invalid input, :invalid');
+ if (firstInvalid && firstInvalid.focus) {
+ firstInvalid.focus();
+ }
+ return;
+ }
+
+ if (!form.checkValidity()) {
+ e.preventDefault();
+ form.reportValidity();
+ }
+ });
+})();
diff --git a/includes/class-activator.php b/includes/class-activator.php
new file mode 100644
index 0000000..c10aee4
--- /dev/null
+++ b/includes/class-activator.php
@@ -0,0 +1,31 @@
+ $value ) {
+ if ( false === get_option( $key ) ) {
+ add_option( $key, $value );
+ }
+ }
+
+ flush_rewrite_rules();
+ }
+}
diff --git a/includes/class-autoloader.php b/includes/class-autoloader.php
new file mode 100644
index 0000000..399fca4
--- /dev/null
+++ b/includes/class-autoloader.php
@@ -0,0 +1,57 @@
+ 'includes/class-plugin.php',
+ 'TON_Reg_Activator' => 'includes/class-activator.php',
+ 'TON_Reg_Deactivator' => 'includes/class-deactivator.php',
+ 'TON_Reg_Uninstaller' => 'includes/class-uninstaller.php',
+ 'TON_Reg_Database' => 'includes/class-database.php',
+ 'TON_Reg_Defaults' => 'includes/class-defaults.php',
+ 'TON_Reg_Captcha' => 'includes/class-captcha.php',
+ 'TON_Reg_User_Manager' => 'includes/class-user-manager.php',
+ 'TON_Reg_Mailer' => 'includes/class-mailer.php',
+ 'TON_Reg_Gdpr' => 'includes/class-gdpr.php',
+ 'TON_Reg_Documents' => 'includes/class-documents.php',
+ 'TON_Reg_Registration_Form' => 'includes/class-registration-form.php',
+ 'TON_Reg_Registration_Handler' => 'includes/class-registration-handler.php',
+ 'TON_Reg_Admin' => 'admin/class-admin.php',
+ 'TON_Reg_Registration_List_Table' => 'admin/class-registration-list-table.php',
+ );
+
+ if ( ! isset( $map[ $class ] ) ) {
+ return;
+ }
+
+ $file = TON_REG_PLUGIN_DIR . $map[ $class ];
+ if ( is_readable( $file ) ) {
+ require_once $file;
+ }
+ }
+}
diff --git a/includes/class-captcha.php b/includes/class-captcha.php
new file mode 100644
index 0000000..54a8b44
--- /dev/null
+++ b/includes/class-captcha.php
@@ -0,0 +1,138 @@
+ $a,
+ 'b' => $b,
+ 'question' => sprintf(
+ /* translators: 1: number, 2: number */
+ __( 'Quanto fa %1$d + %2$d?', 'ton-italia-registration' ),
+ $a,
+ $b
+ ),
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public static function check_rate_limit() {
+ $ip = self::get_client_ip();
+ $key = 'ton_reg_rl_' . md5( $ip );
+ $max = max( 1, (int) get_option( 'ton_reg_rate_limit_max', 5 ) );
+ $window = max( 60, (int) get_option( 'ton_reg_rate_limit_window', 900 ) );
+ $count = (int) get_transient( $key );
+
+ if ( $count >= $max ) {
+ return false;
+ }
+
+ set_transient( $key, $count + 1, $window );
+ return true;
+ }
+
+ /**
+ * @param array $post POST data.
+ * @return true|WP_Error
+ */
+ public static function validate( $post ) {
+ if ( ! self::is_enabled() ) {
+ return true;
+ }
+
+ if ( ! empty( $post[ self::FIELD_HONEYPOT ] ) ) {
+ return new WP_Error( 'ton_reg_spam', __( 'Invio non valido.', 'ton-italia-registration' ) );
+ }
+
+ $token = isset( $post[ self::FIELD_TOKEN ] ) ? sanitize_text_field( wp_unslash( $post[ self::FIELD_TOKEN ] ) ) : '';
+ if ( ! $token || ! get_transient( self::token_key( $token ) ) ) {
+ return new WP_Error( 'ton_reg_token', __( 'Sessione scaduta. Ricarica la pagina e riprova.', 'ton-italia-registration' ) );
+ }
+
+ $issued = (int) get_transient( self::token_key( $token ) );
+ if ( time() - $issued < 3 ) {
+ return new WP_Error( 'ton_reg_fast', __( 'Attendi qualche secondo prima di inviare.', 'ton-italia-registration' ) );
+ }
+
+ $expected = get_transient( 'ton_reg_cap_' . md5( $token ) );
+ $answer = isset( $post[ self::FIELD_MATH ] ) ? (int) $post[ self::FIELD_MATH ] : -1;
+
+ if ( false === $expected || (int) $expected !== $answer ) {
+ return new WP_Error( 'ton_reg_captcha', __( 'Risposta di verifica non corretta.', 'ton-italia-registration' ) );
+ }
+
+ delete_transient( 'ton_reg_cap_' . md5( $token ) );
+ delete_transient( self::token_key( $token ) );
+
+ return true;
+ }
+
+ /**
+ * @return string
+ */
+ public static function get_client_ip() {
+ $ip = '';
+ if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
+ $parts = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
+ $ip = trim( $parts[0] );
+ } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
+ $ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
+ }
+ return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0';
+ }
+}
diff --git a/includes/class-database.php b/includes/class-database.php
new file mode 100644
index 0000000..afa900e
--- /dev/null
+++ b/includes/class-database.php
@@ -0,0 +1,238 @@
+prefix . TON_REG_TABLE;
+ }
+
+ /**
+ * Create registrations table.
+ */
+ public static function create_table() {
+ global $wpdb;
+
+ $table = self::table_name();
+ $charset = $wpdb->get_charset_collate();
+
+ $sql = "CREATE TABLE {$table} (
+ id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ user_id bigint(20) unsigned DEFAULT NULL,
+ cognome varchar(100) NOT NULL DEFAULT '',
+ nome varchar(100) NOT NULL DEFAULT '',
+ luogo_nascita varchar(120) NOT NULL DEFAULT '',
+ provincia_nascita char(2) NOT NULL DEFAULT '',
+ data_nascita date DEFAULT NULL,
+ codice_fiscale char(16) NOT NULL DEFAULT '',
+ comune_residenza varchar(120) NOT NULL DEFAULT '',
+ provincia_residenza char(2) NOT NULL DEFAULT '',
+ indirizzo varchar(200) NOT NULL DEFAULT '',
+ numero_civico varchar(20) NOT NULL DEFAULT '',
+ cap char(10) NOT NULL DEFAULT '',
+ telefono varchar(40) NOT NULL DEFAULT '',
+ email varchar(190) NOT NULL DEFAULT '',
+ luogo_dichiarazione varchar(120) NOT NULL DEFAULT '',
+ data_dichiarazione date DEFAULT NULL,
+ bonifico_effettuato tinyint(1) NOT NULL DEFAULT 0,
+ consenso_statuto tinyint(1) NOT NULL DEFAULT 0,
+ consenso_privacy tinyint(1) NOT NULL DEFAULT 0,
+ consenso_newsletter tinyint(1) NOT NULL DEFAULT 0,
+ registration_ip varchar(45) NOT NULL DEFAULT '',
+ user_agent varchar(255) NOT NULL DEFAULT '',
+ privacy_text_version varchar(32) NOT NULL DEFAULT '',
+ consent_statuto_at datetime DEFAULT NULL,
+ consent_privacy_at datetime DEFAULT NULL,
+ status varchar(20) NOT NULL DEFAULT 'pending',
+ admin_notes text NULL,
+ libro_associati_date date DEFAULT NULL,
+ doc_identita_id bigint(20) unsigned DEFAULT NULL,
+ doc_bonifico_id bigint(20) unsigned DEFAULT NULL,
+ doc_pagamento_id bigint(20) unsigned DEFAULT NULL,
+ gdpr_log longtext NULL,
+ created_at datetime NOT NULL,
+ updated_at datetime NOT NULL,
+ anonymized_at datetime DEFAULT NULL,
+ PRIMARY KEY (id),
+ KEY email (email),
+ KEY codice_fiscale (codice_fiscale),
+ KEY status (status),
+ KEY created_at (created_at),
+ KEY user_id (user_id)
+ ) {$charset};";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql );
+ }
+
+ /**
+ * @param array $data Row data.
+ * @return int|false Insert ID or false.
+ */
+ public static function insert( $data ) {
+ global $wpdb;
+
+ $now = current_time( 'mysql', true );
+ $data['created_at'] = $now;
+ $data['updated_at'] = $now;
+
+ $result = $wpdb->insert( self::table_name(), $data );
+ return false === $result ? false : (int) $wpdb->insert_id;
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @param array $data Data.
+ * @return bool
+ */
+ public static function update( $id, $data ) {
+ global $wpdb;
+
+ $data['updated_at'] = current_time( 'mysql', true );
+
+ return false !== $wpdb->update(
+ self::table_name(),
+ $data,
+ array( 'id' => $id ),
+ null,
+ array( '%d' )
+ );
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @return object|null
+ */
+ public static function get( $id ) {
+ global $wpdb;
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . ' WHERE id = %d',
+ $id
+ )
+ );
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @return bool
+ */
+ public static function delete( $id ) {
+ global $wpdb;
+
+ return false !== $wpdb->delete(
+ self::table_name(),
+ array( 'id' => $id ),
+ array( '%d' )
+ );
+ }
+
+ /**
+ * @param string $email Email.
+ * @param string $cf Codice fiscale.
+ * @return object|null
+ */
+ public static function find_active_by_email_or_cf( $email, $cf ) {
+ global $wpdb;
+
+ return $wpdb->get_row(
+ $wpdb->prepare(
+ 'SELECT * FROM ' . self::table_name() . " WHERE ( email = %s OR codice_fiscale = %s ) AND status IN ('pending','admitted') AND anonymized_at IS NULL LIMIT 1",
+ $email,
+ $cf
+ )
+ );
+ }
+
+ /**
+ * @param array $args Query args.
+ * @return array
+ */
+ public static function query( $args = array() ) {
+ global $wpdb;
+
+ $defaults = array(
+ 'status' => '',
+ 'search' => '',
+ 'limit' => 20,
+ 'offset' => 0,
+ 'orderby' => 'created_at',
+ 'order' => 'DESC',
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ $where = array( '1=1' );
+ $params = array();
+
+ if ( ! empty( $args['status'] ) ) {
+ $where[] = 'status = %s';
+ $params[] = $args['status'];
+ }
+
+ if ( ! empty( $args['search'] ) ) {
+ $like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
+ $where[] = '( cognome LIKE %s OR nome LIKE %s OR email LIKE %s OR codice_fiscale LIKE %s )';
+ $params = array_merge( $params, array( $like, $like, $like, $like ) );
+ }
+
+ $allowed_orderby = array( 'created_at', 'cognome', 'nome', 'email', 'status' );
+ $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at';
+ $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
+
+ $sql = 'SELECT * FROM ' . self::table_name() . ' WHERE ' . implode( ' AND ', $where )
+ . " ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
+
+ $params[] = (int) $args['limit'];
+ $params[] = (int) $args['offset'];
+
+ if ( ! empty( $params ) ) {
+ $sql = $wpdb->prepare( $sql, $params );
+ }
+
+ return $wpdb->get_results( $sql );
+ }
+
+ /**
+ * @param array $args Args.
+ * @return int
+ */
+ public static function count( $args = array() ) {
+ global $wpdb;
+
+ $where = array( '1=1' );
+ $params = array();
+
+ if ( ! empty( $args['status'] ) ) {
+ $where[] = 'status = %s';
+ $params[] = $args['status'];
+ }
+
+ if ( ! empty( $args['search'] ) ) {
+ $like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
+ $where[] = '( cognome LIKE %s OR nome LIKE %s OR email LIKE %s OR codice_fiscale LIKE %s )';
+ $params = array_merge( $params, array( $like, $like, $like, $like ) );
+ }
+
+ $sql = 'SELECT COUNT(*) FROM ' . self::table_name() . ' WHERE ' . implode( ' AND ', $where );
+
+ if ( ! empty( $params ) ) {
+ $sql = $wpdb->prepare( $sql, $params );
+ }
+
+ return (int) $wpdb->get_var( $sql );
+ }
+}
diff --git a/includes/class-deactivator.php b/includes/class-deactivator.php
new file mode 100644
index 0000000..cf1a6d4
--- /dev/null
+++ b/includes/class-deactivator.php
@@ -0,0 +1,21 @@
+
+ */
+ public static function options() {
+ return array(
+ 'ton_reg_social_year' => '2026',
+ 'ton_reg_membership_fee' => '10,00',
+ 'ton_reg_beneficiary' => 'TON ITALIA ODV',
+ 'ton_reg_iban' => 'IT38N0501811700000020000843',
+ 'ton_reg_payment_instructions' => self::payment_instructions(),
+ 'ton_reg_statuto_declaration' => self::statuto_declaration(),
+ 'ton_reg_privacy_notice' => self::privacy_notice(),
+ 'ton_reg_newsletter_label' => self::newsletter_label(),
+ 'ton_reg_minor_notice' => self::minor_notice(),
+ 'ton_reg_success_message' => 'Grazie. La tua domanda di iscrizione è stata ricevuta. Riceverai un\'email di conferma con i prossimi passaggi.
',
+ 'ton_reg_admin_email' => get_option( 'admin_email' ),
+ 'ton_reg_gdpr_contact' => 'qeshet.tonitalia@gmail.com',
+ 'ton_reg_email_admin_subject' => '[TON ITALIA ODV] Nuova domanda di iscrizione socio',
+ 'ton_reg_email_admin_body' => self::email_admin_body(),
+ 'ton_reg_email_user_subject' => 'Conferma iscrizione TON ITALIA ODV',
+ 'ton_reg_email_user_body' => self::email_user_body(),
+ 'ton_reg_captcha_enabled' => '1',
+ 'ton_reg_rate_limit_max' => '5',
+ 'ton_reg_rate_limit_window' => '900',
+ 'ton_reg_newsletter_required' => '0',
+ 'ton_reg_privacy_version' => '1',
+ 'ton_reg_delete_users_on_uninstall' => '0',
+ );
+ }
+
+ /**
+ * @return string
+ */
+ private static function payment_instructions() {
+ return 'Quota associativa annuale: € {quota} per l\'anno sociale {anno} .
'
+ . 'Effettua un bonifico bancario:
'
+ . ''
+ . 'Beneficiario: {beneficiario} '
+ . 'IBAN: {iban} '
+ . 'Causale: tesseramento TON ITALIA ODV anno {anno} di {nome} {cognome} '
+ . ' '
+ . 'L\'iscrizione sarà considerata completa a seguito di ammissione da parte del Direttivo, della ricezione del bonifico e annotazione nel Libro degli associati.
';
+ }
+
+ /**
+ * @return string
+ */
+ private static function statuto_declaration() {
+ return 'Dichiaro ai sensi degli artt. 46 e 47 del D.P.R. 445/2000 di aver preso visione dello Statuto e dei regolamenti interni dell\'Associazione, '
+ . 'di condividerne pienamente i principi, le finalità e le attività dell\'organizzazione, di accettare e rispettare le deliberazioni legalmente adottate '
+ . 'dagli organi sociali e di impegnarmi al pagamento della quota associativa annuale stabilita per l\'anno in corso.';
+ }
+
+ /**
+ * @return string
+ */
+ private static function privacy_notice() {
+ return 'INFORMATIVA BREVE PRIVACY (Regolamento UE 2016/679 - GDPR)
'
+ . 'Ai sensi dell\'art. 13 del GDPR, l\'Associazione TON ITALIA ODV, in qualità di Titolare del trattamento, informa che i dati personali raccolti '
+ . 'saranno utilizzati esclusivamente per la gestione del rapporto associativo, l\'invio di comunicazioni istituzionali e l\'adempimento di obblighi di legge. '
+ . 'I dati non saranno comunicati a terzi né diffusi. Il trattamento avverrà con modalità manuali e informatiche. '
+ . 'I diritti dell\'interessato (artt. 15-22 GDPR) possono essere esercitati scrivendo a qeshet.tonitalia@gmail.com .
';
+ }
+
+ /**
+ * @return string
+ */
+ private static function newsletter_label() {
+ return 'Esprimo il consenso al trattamento dei dati personali per le finalità sopra descritte e per ricevere le newsletter trimestrali di TON ITALIA ODV.';
+ }
+
+ /**
+ * @return string
+ */
+ private static function minor_notice() {
+ return 'Se il richiedente è minorenne, la richiesta deve essere firmata da entrambi i genitori o dal tutore legale.';
+ }
+
+ /**
+ * @return string
+ */
+ private static function email_admin_body() {
+ return "Nuova domanda di iscrizione socio.\n\n"
+ . "Nome: {nome} {cognome}\n"
+ . "Email: {email}\n"
+ . "Codice fiscale: {codice_fiscale}\n"
+ . "IP: {ip}\n\n"
+ . "Gestisci la richiesta: {admin_url}";
+ }
+
+ /**
+ * @return string
+ */
+ private static function email_user_body() {
+ return "Gentile {nome} {cognome},\n\n"
+ . "abbiamo ricevuto la tua domanda di ammissione a socio di TON ITALIA ODV.\n\n"
+ . "È stato creato un account WordPress con ruolo Subscriber (accesso limitato, senza permessi di amministrazione).\n"
+ . "Email di accesso: {email}\n\n"
+ . "Per impostare la password visita: {login_url}\n\n"
+ . "Ricorda di effettuare il bonifico della quota associativa secondo le istruzioni ricevute.\n\n"
+ . "Ai sensi del GDPR puoi richiedere in qualsiasi momento la cancellazione o l'anonimizzazione dei tuoi dati scrivendo a {gdpr_contact}.\n\n"
+ . "Cordiali saluti,\nTON ITALIA ODV";
+ }
+}
diff --git a/includes/class-documents.php b/includes/class-documents.php
new file mode 100644
index 0000000..dfc8496
--- /dev/null
+++ b/includes/class-documents.php
@@ -0,0 +1,353 @@
+
+ */
+ public static function get_type_config() {
+ return array(
+ self::TYPE_IDENTITA => array(
+ 'column' => 'doc_identita_id',
+ 'label' => __( 'Documento di identità', 'ton-italia-registration' ),
+ 'title' => __( 'Documento identità', 'ton-italia-registration' ),
+ ),
+ self::TYPE_BONIFICO => array(
+ 'column' => 'doc_bonifico_id',
+ 'label' => __( 'Ricevuta bonifico', 'ton-italia-registration' ),
+ 'title' => __( 'Ricevuta bonifico', 'ton-italia-registration' ),
+ ),
+ self::TYPE_PAGAMENTO => array(
+ 'column' => 'doc_pagamento_id',
+ 'label' => __( 'Ricevuta pagamento', 'ton-italia-registration' ),
+ 'title' => __( 'Ricevuta pagamento', 'ton-italia-registration' ),
+ ),
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public static function get_types() {
+ return array_keys( self::get_type_config() );
+ }
+
+ /**
+ * @param string $type Document type.
+ * @return array{column:string, label:string, title:string}|null
+ */
+ public static function get_config( $type ) {
+ $config = self::get_type_config();
+ return $config[ $type ] ?? null;
+ }
+
+ /**
+ * @param string $type Document type.
+ * @return string
+ */
+ public static function get_column( $type ) {
+ $config = self::get_config( $type );
+ return $config ? $config['column'] : '';
+ }
+
+ /**
+ * Install base media folder.
+ */
+ public static function install_folders() {
+ $path = self::get_base_path();
+ wp_mkdir_p( $path );
+ self::write_index_file( $path );
+ }
+
+ /**
+ * Remove entire documents tree from uploads.
+ */
+ public static function uninstall_folders() {
+ $path = self::get_base_path();
+ if ( is_dir( $path ) ) {
+ self::delete_directory( $path );
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public static function get_base_path() {
+ $upload = wp_upload_dir();
+ return trailingslashit( $upload['basedir'] ) . self::BASE_FOLDER;
+ }
+
+ /**
+ * @return string
+ */
+ public static function get_base_url() {
+ $upload = wp_upload_dir();
+ $folder = str_replace( ' ', '%20', self::BASE_FOLDER );
+ return trailingslashit( $upload['baseurl'] ) . $folder;
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @return string
+ */
+ public static function member_folder_slug( $row ) {
+ $cf = strtoupper( preg_replace( '/\s+/', '', (string) $row->codice_fiscale ) );
+ return sanitize_title( $row->nome . '-' . $row->cognome . '-' . $cf );
+ }
+
+ /**
+ * @param object $row Registration row.
+ * @return string
+ */
+ public static function get_member_path( $row ) {
+ return trailingslashit( self::get_base_path() ) . self::member_folder_slug( $row );
+ }
+
+ /**
+ * @param object $row Registration row.
+ */
+ public static function ensure_member_folder( $row ) {
+ self::install_folders();
+ $path = self::get_member_path( $row );
+ wp_mkdir_p( $path );
+ self::write_index_file( $path );
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param string $type Document type.
+ * @param array $file $_FILES item.
+ * @return int|WP_Error Attachment ID.
+ */
+ public static function upload( $registration_id, $type, $file ) {
+ $row = TON_Reg_Database::get( $registration_id );
+ if ( ! $row ) {
+ return new WP_Error( 'not_found', __( 'Iscrizione non trovata.', 'ton-italia-registration' ) );
+ }
+
+ $config = self::get_config( $type );
+ if ( ! $config ) {
+ return new WP_Error( 'invalid_type', __( 'Tipo documento non valido.', 'ton-italia-registration' ) );
+ }
+
+ if ( empty( $file['name'] ) || ! empty( $file['error'] ) ) {
+ return new WP_Error( 'no_file', __( 'Nessun file selezionato o errore nel caricamento.', 'ton-italia-registration' ) );
+ }
+
+ self::ensure_member_folder( $row );
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+
+ $allowed = array(
+ 'pdf' => 'application/pdf',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'png' => 'image/png',
+ );
+
+ $check = wp_check_filetype( $file['name'], $allowed );
+ if ( empty( $check['ext'] ) || empty( $check['type'] ) ) {
+ return new WP_Error( 'mime', __( 'Formato non consentito. Usa PDF, JPG o PNG.', 'ton-italia-registration' ) );
+ }
+
+ self::$upload_member_slug = self::member_folder_slug( $row );
+ add_filter( 'upload_dir', array( __CLASS__, 'filter_upload_dir' ) );
+
+ $attachment_id = media_handle_sideload(
+ $file,
+ 0,
+ sprintf(
+ /* translators: 1: document type, 2: full name */
+ __( '%1$s — %2$s', 'ton-italia-registration' ),
+ $config['title'],
+ $row->nome . ' ' . $row->cognome
+ )
+ );
+
+ remove_filter( 'upload_dir', array( __CLASS__, 'filter_upload_dir' ) );
+ self::$upload_member_slug = null;
+
+ if ( is_wp_error( $attachment_id ) ) {
+ return $attachment_id;
+ }
+
+ update_post_meta( $attachment_id, '_ton_reg_registration_id', (int) $registration_id );
+ update_post_meta( $attachment_id, '_ton_reg_document_type', $type );
+
+ $column = $config['column'];
+ $old_id = (int) ( $row->$column ?? 0 );
+ if ( $old_id && $old_id !== $attachment_id ) {
+ wp_delete_attachment( $old_id, true );
+ }
+
+ TON_Reg_Database::update(
+ $registration_id,
+ array( $column => $attachment_id )
+ );
+
+ return $attachment_id;
+ }
+
+ /**
+ * @param int $registration_id Registration ID.
+ * @param string $type Document type.
+ * @return bool|WP_Error
+ */
+ public static function delete_document( $registration_id, $type ) {
+ $row = TON_Reg_Database::get( $registration_id );
+ if ( ! $row ) {
+ return new WP_Error( 'not_found', __( 'Iscrizione non trovata.', 'ton-italia-registration' ) );
+ }
+
+ $column = self::get_column( $type );
+ if ( ! $column ) {
+ return new WP_Error( 'invalid_type', __( 'Tipo documento non valido.', 'ton-italia-registration' ) );
+ }
+
+ $attachment_id = (int) ( $row->$column ?? 0 );
+
+ if ( $attachment_id ) {
+ wp_delete_attachment( $attachment_id, true );
+ }
+
+ TON_Reg_Database::update( $registration_id, array( $column => null ) );
+
+ return true;
+ }
+
+ /**
+ * Delete all documents for a registration and optionally its folder.
+ *
+ * @param int $registration_id Registration ID.
+ * @param bool $remove_folder Remove member folder.
+ */
+ public static function delete_all_for_registration( $registration_id, $remove_folder = true ) {
+ $row = TON_Reg_Database::get( $registration_id );
+ if ( ! $row ) {
+ return;
+ }
+
+ foreach ( self::get_types() as $type ) {
+ self::delete_document( $registration_id, $type );
+ }
+
+ if ( $remove_folder ) {
+ $path = self::get_member_path( $row );
+ if ( is_dir( $path ) ) {
+ self::delete_directory( $path );
+ }
+ }
+ }
+
+ /**
+ * @param array $dirs Upload dirs.
+ * @return array
+ */
+ public static function filter_upload_dir( $dirs ) {
+ if ( ! self::$upload_member_slug ) {
+ return $dirs;
+ }
+
+ $subdir = '/' . self::BASE_FOLDER . '/' . self::$upload_member_slug;
+ $dirs['subdir'] = $subdir;
+ $dirs['path'] = $dirs['basedir'] . $subdir;
+ $dirs['url'] = $dirs['baseurl'] . $subdir;
+
+ if ( ! is_dir( $dirs['path'] ) ) {
+ wp_mkdir_p( $dirs['path'] );
+ self::write_index_file( $dirs['path'] );
+ }
+
+ return $dirs;
+ }
+
+ /**
+ * @param int $attachment_id Attachment ID.
+ * @return array{url:string,name:string,id:int}|null
+ */
+ public static function get_file_info( $attachment_id ) {
+ if ( ! $attachment_id ) {
+ return null;
+ }
+ $url = wp_get_attachment_url( $attachment_id );
+ if ( ! $url ) {
+ return null;
+ }
+ return array(
+ 'id' => (int) $attachment_id,
+ 'url' => $url,
+ 'name' => basename( get_attached_file( $attachment_id ) ),
+ );
+ }
+
+ /**
+ * @param string $type Document type key.
+ * @return string
+ */
+ public static function type_label( $type ) {
+ $config = self::get_config( $type );
+ return $config ? $config['label'] : $type;
+ }
+
+ /**
+ * @param string $path Directory path.
+ */
+ private static function write_index_file( $path ) {
+ $file = trailingslashit( $path ) . 'index.php';
+ if ( ! file_exists( $file ) ) {
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
+ file_put_contents( $file, "|null
+ */
+ public static function export( $id ) {
+ $row = TON_Reg_Database::get( $id );
+ if ( ! $row ) {
+ return null;
+ }
+
+ $data = (array) $row;
+ if ( $row->user_id ) {
+ $user = get_userdata( (int) $row->user_id );
+ if ( $user ) {
+ $data['wp_user'] = array(
+ 'ID' => $user->ID,
+ 'user_login' => $user->user_login,
+ 'user_email' => $user->user_email,
+ 'display_name' => $user->display_name,
+ );
+ $meta = array();
+ foreach ( get_user_meta( (int) $row->user_id ) as $key => $values ) {
+ if ( 0 === strpos( $key, 'ton_reg_' ) ) {
+ $meta[ $key ] = $values[0] ?? '';
+ }
+ }
+ $data['user_meta'] = $meta;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @param string $admin_note Note.
+ * @return bool|WP_Error
+ */
+ public static function anonymize( $id, $admin_note = '' ) {
+ $row = TON_Reg_Database::get( $id );
+ if ( ! $row ) {
+ return new WP_Error( 'not_found', __( 'Registrazione non trovata.', 'ton-italia-registration' ) );
+ }
+
+ if ( $row->anonymized_at ) {
+ return new WP_Error( 'already', __( 'Già anonimizzata.', 'ton-italia-registration' ) );
+ }
+
+ $placeholder = 'ANONIMIZZATO';
+ $anon_email = 'anon-' . $id . '@removed.local';
+
+ TON_Reg_Database::update(
+ $id,
+ array(
+ 'cognome' => $placeholder,
+ 'nome' => $placeholder,
+ 'luogo_nascita' => $placeholder,
+ 'provincia_nascita' => 'XX',
+ 'data_nascita' => null,
+ 'codice_fiscale' => str_repeat( 'X', 16 ),
+ 'comune_residenza' => $placeholder,
+ 'provincia_residenza' => 'XX',
+ 'indirizzo' => $placeholder,
+ 'numero_civico' => '0',
+ 'cap' => '00000',
+ 'telefono' => '0000000000',
+ 'email' => $anon_email,
+ 'luogo_dichiarazione' => $placeholder,
+ 'data_dichiarazione' => null,
+ 'registration_ip' => '0.0.0.0',
+ 'user_agent' => '',
+ 'anonymized_at' => current_time( 'mysql', true ),
+ 'gdpr_log' => self::append_log( $row->gdpr_log, 'anonymize', $admin_note ),
+ )
+ );
+
+ TON_Reg_Documents::delete_all_for_registration( $id, true );
+
+ if ( $row->user_id ) {
+ $uid = (int) $row->user_id;
+ wp_update_user(
+ array(
+ 'ID' => $uid,
+ 'user_email' => $anon_email,
+ 'first_name' => $placeholder,
+ 'last_name' => $placeholder,
+ 'display_name' => $placeholder,
+ )
+ );
+ $meta_keys = array(
+ 'ton_reg_cognome',
+ 'ton_reg_nome',
+ 'ton_reg_luogo_nascita',
+ 'ton_reg_codice_fiscale',
+ 'ton_reg_telefono',
+ 'ton_reg_indirizzo',
+ );
+ foreach ( $meta_keys as $key ) {
+ update_user_meta( $uid, $key, $placeholder );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param int $id Registration ID.
+ * @param string $note Admin note.
+ * @return bool|WP_Error
+ */
+ public static function erase( $id, $note = '' ) {
+ $row = TON_Reg_Database::get( $id );
+ if ( ! $row ) {
+ return new WP_Error( 'not_found', __( 'Registrazione non trovata.', 'ton-italia-registration' ) );
+ }
+
+ TON_Reg_Documents::delete_all_for_registration( $id, true );
+
+ if ( $row->user_id ) {
+ require_once ABSPATH . 'wp-admin/includes/user.php';
+ wp_delete_user( (int) $row->user_id );
+ }
+
+ TON_Reg_Database::delete( $id );
+ self::log_action( 'erase', $id, $note );
+
+ return true;
+ }
+
+ /**
+ * @param string|null $existing Existing log JSON.
+ * @param string $action Action.
+ * @param string $note Note.
+ * @return string JSON.
+ */
+ private static function append_log( $existing, $action, $note ) {
+ $log = array();
+ if ( $existing ) {
+ $decoded = json_decode( $existing, true );
+ if ( is_array( $decoded ) ) {
+ $log = $decoded;
+ }
+ }
+ $log[] = array(
+ 'action' => $action,
+ 'time' => current_time( 'mysql', true ),
+ 'admin' => get_current_user_id(),
+ 'note' => sanitize_text_field( $note ),
+ );
+ return wp_json_encode( $log );
+ }
+
+ /**
+ * @param string $action Action.
+ * @param int $id ID.
+ * @param string $note Note.
+ */
+ private static function log_action( $action, $id, $note ) {
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( sprintf( 'TON Reg GDPR %s #%d: %s', $action, $id, $note ) );
+ }
+ }
+}
diff --git a/includes/class-mailer.php b/includes/class-mailer.php
new file mode 100644
index 0000000..82483c3
--- /dev/null
+++ b/includes/class-mailer.php
@@ -0,0 +1,71 @@
+email;
+ $subject = get_option( 'ton_reg_email_user_subject', '' );
+ $body = get_option( 'ton_reg_email_user_body', '' );
+
+ $replacements = self::replacements( $registration, $user_id );
+ $subject = strtr( $subject, $replacements );
+ $body = strtr( $body, $replacements );
+
+ wp_mail( $to, $subject, $body );
+
+ // Standard WP new user notification with password reset link.
+ if ( function_exists( 'wp_send_new_user_notifications' ) ) {
+ wp_send_new_user_notifications( $user_id, 'user' );
+ }
+ }
+
+ /**
+ * @param object $registration Registration.
+ * @param int $user_id User ID.
+ * @return array
+ */
+ private static function replacements( $registration, $user_id ) {
+ $admin_url = admin_url( 'admin.php?page=ton-registrations&action=view&id=' . (int) $registration->id );
+
+ return array(
+ '{nome}' => $registration->nome,
+ '{cognome}' => $registration->cognome,
+ '{email}' => $registration->email,
+ '{codice_fiscale}' => $registration->codice_fiscale,
+ '{ip}' => $registration->registration_ip,
+ '{login_url}' => wp_lostpassword_url(),
+ '{gdpr_contact}' => get_option( 'ton_reg_gdpr_contact', '' ),
+ '{admin_url}' => $admin_url,
+ );
+ }
+}
diff --git a/includes/class-plugin.php b/includes/class-plugin.php
new file mode 100644
index 0000000..41c549b
--- /dev/null
+++ b/includes/class-plugin.php
@@ -0,0 +1,104 @@
+maybe_upgrade();
+ }
+
+ /**
+ * Run DB / folder upgrades when plugin files are updated.
+ */
+ private function maybe_upgrade() {
+ $db_version = (string) get_option( 'ton_reg_db_version', '0' );
+
+ if ( version_compare( $db_version, '1.1', '<' ) ) {
+ TON_Reg_Database::create_table();
+ TON_Reg_Documents::install_folders();
+ $db_version = '1.1';
+ }
+
+ if ( version_compare( $db_version, '1.2', '<' ) ) {
+ TON_Reg_Database::create_table();
+ $db_version = '1.2';
+ }
+
+ update_option( 'ton_reg_db_version', $db_version );
+ }
+
+ /**
+ * Prevent subscribers from accessing wp-admin (except AJAX).
+ */
+ public function block_subscriber_admin() {
+ if ( ! is_user_logged_in() || wp_doing_ajax() ) {
+ return;
+ }
+
+ $user = wp_get_current_user();
+ if ( in_array( 'subscriber', (array) $user->roles, true ) && ! current_user_can( 'manage_options' ) ) {
+ wp_safe_redirect( home_url() );
+ exit;
+ }
+ }
+
+ /**
+ * @param bool $show Whether to show admin bar.
+ * @return bool
+ */
+ public function hide_admin_bar_for_subscribers( $show ) {
+ if ( ! is_user_logged_in() ) {
+ return $show;
+ }
+ $user = wp_get_current_user();
+ if ( in_array( 'subscriber', (array) $user->roles, true ) && ! current_user_can( 'manage_options' ) ) {
+ return false;
+ }
+ return $show;
+ }
+}
diff --git a/includes/class-registration-form.php b/includes/class-registration-form.php
new file mode 100644
index 0000000..5620a11
--- /dev/null
+++ b/includes/class-registration-form.php
@@ -0,0 +1,282 @@
+ __( 'Compila questo campo.', 'ton-italia-registration' ),
+ 'msgEmail' => __( 'Inserisci un indirizzo email valido.', 'ton-italia-registration' ),
+ 'msgCf' => __( 'Codice fiscale non valido (16 caratteri).', 'ton-italia-registration' ),
+ 'msgCap' => __( 'Il CAP deve essere di 5 cifre.', 'ton-italia-registration' ),
+ 'msgProvincia' => __( 'Inserisci la sigla provincia (2 lettere).', 'ton-italia-registration' ),
+ 'msgDate' => __( 'Inserisci la data nel formato gg-mm-aaaa.', 'ton-italia-registration' ),
+ 'msgCheckbox' => __( 'Devi selezionare questa casella per continuare.', 'ton-italia-registration' ),
+ 'msgCaptcha' => __( 'Inserisci la risposta alla verifica anti-spam.', 'ton-italia-registration' ),
+ )
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ private static function page_has_shortcode() {
+ global $post;
+ return $post instanceof WP_Post && has_shortcode( $post->post_content, 'ton_registration_form' );
+ }
+
+ /**
+ * @param array $atts Attributes.
+ * @return string
+ */
+ public static function render_shortcode( $atts ) {
+ $atts = shortcode_atts(
+ array( 'class' => '' ),
+ $atts,
+ 'ton_registration_form'
+ );
+
+ if ( isset( $_GET['ton_reg'] ) && 'success' === $_GET['ton_reg'] ) {
+ return self::render_message( 'success' );
+ }
+
+ $token = TON_Reg_Captcha::create_token();
+ $challenge = TON_Reg_Captcha::generate_challenge( $token );
+
+ ob_start();
+ self::render_form( $atts['class'], $token, $challenge );
+ return ob_get_clean();
+ }
+
+ /**
+ * @param string $type success|error.
+ * @return string
+ */
+ private static function render_message( $type ) {
+ if ( 'success' === $type ) {
+ $html = get_option( 'ton_reg_success_message', '' );
+ return '' . wp_kses_post( $html ) . '
';
+ }
+ $msg = isset( $_GET['ton_msg'] ) ? sanitize_text_field( wp_unslash( rawurldecode( $_GET['ton_msg'] ) ) ) : '';
+ return '';
+ }
+
+ /**
+ * @param string $extra_class Extra CSS class.
+ * @param string $token Form token.
+ * @param array $challenge Captcha challenge.
+ */
+ private static function render_form( $extra_class, $token, $challenge ) {
+ $payment = self::replace_payment_placeholders( get_option( 'ton_reg_payment_instructions', '' ) );
+ $class = 'ton-reg-form' . ( $extra_class ? ' ' . esc_attr( $extra_class ) : '' );
+
+ if ( isset( $_GET['ton_reg'] ) && 'error' === $_GET['ton_reg'] ) {
+ echo self::render_message( 'error' );
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+ *
+
+
+
+
+
+ />
+
+
+
+ */
+ private static function placeholders() {
+ return array(
+ 'cognome' => __( 'Inserisci il cognome', 'ton-italia-registration' ),
+ 'nome' => __( 'Inserisci il nome', 'ton-italia-registration' ),
+ 'luogo_nascita' => __( 'Comune o stato di nascita', 'ton-italia-registration' ),
+ 'provincia_nascita' => __( 'Sigla provincia, es. VR', 'ton-italia-registration' ),
+ 'data_nascita' => __( 'gg-mm-aaaa', 'ton-italia-registration' ),
+ 'codice_fiscale' => __( '16 caratteri alfanumerici', 'ton-italia-registration' ),
+ 'comune_residenza' => __( 'Comune di residenza', 'ton-italia-registration' ),
+ 'provincia_residenza' => __( 'Sigla provincia, es. MI', 'ton-italia-registration' ),
+ 'indirizzo' => __( 'Via o piazza', 'ton-italia-registration' ),
+ 'numero_civico' => __( 'Numero civico', 'ton-italia-registration' ),
+ 'cap' => __( '5 cifre', 'ton-italia-registration' ),
+ 'telefono' => __( 'Telefono o cellulare', 'ton-italia-registration' ),
+ 'email' => __( 'nome@esempio.it', 'ton-italia-registration' ),
+ 'luogo_dichiarazione' => __( 'Comune della dichiarazione', 'ton-italia-registration' ),
+ 'data_dichiarazione' => __( 'gg-mm-aaaa', 'ton-italia-registration' ),
+ );
+ }
+
+ /**
+ * @param string $key Field key.
+ * @return string
+ */
+ private static function old( $key ) {
+ // No session storage for security; empty on fresh load.
+ return '';
+ }
+
+ /**
+ * @param string $html HTML template.
+ * @return string
+ */
+ private static function replace_payment_placeholders( $html ) {
+ return strtr(
+ $html,
+ array(
+ '{quota}' => esc_html( get_option( 'ton_reg_membership_fee', '10,00' ) ),
+ '{anno}' => esc_html( get_option( 'ton_reg_social_year', '2026' ) ),
+ '{beneficiario}' => esc_html( get_option( 'ton_reg_beneficiary', '' ) ),
+ '{iban}' => esc_html( get_option( 'ton_reg_iban', '' ) ),
+ '{nome}' => '',
+ '{cognome}' => '',
+ )
+ );
+ }
+}
diff --git a/includes/class-registration-handler.php b/includes/class-registration-handler.php
new file mode 100644
index 0000000..420782d
--- /dev/null
+++ b/includes/class-registration-handler.php
@@ -0,0 +1,272 @@
+get_error_message() );
+ }
+
+ $data = self::sanitize_post( $_POST );
+ $valid = self::validate( $data );
+ if ( is_wp_error( $valid ) ) {
+ self::redirect_error( $redirect, $valid->get_error_message() );
+ }
+
+ $existing = TON_Reg_Database::find_active_by_email_or_cf( $data['email'], $data['codice_fiscale'] );
+ if ( $existing ) {
+ self::redirect_error( $redirect, __( 'Esiste già una richiesta attiva con questa email o codice fiscale.', 'ton-italia-registration' ) );
+ }
+
+ $user_id = TON_Reg_User_Manager::create_subscriber( $data );
+ if ( is_wp_error( $user_id ) ) {
+ self::redirect_error( $redirect, $user_id->get_error_message() );
+ }
+
+ $now = current_time( 'mysql', true );
+ $privacy = (string) get_option( 'ton_reg_privacy_version', '1' );
+
+ $row = array(
+ 'user_id' => $user_id,
+ 'cognome' => $data['cognome'],
+ 'nome' => $data['nome'],
+ 'luogo_nascita' => $data['luogo_nascita'],
+ 'provincia_nascita' => $data['provincia_nascita'],
+ 'data_nascita' => $data['data_nascita'],
+ 'codice_fiscale' => $data['codice_fiscale'],
+ 'comune_residenza' => $data['comune_residenza'],
+ 'provincia_residenza' => $data['provincia_residenza'],
+ 'indirizzo' => $data['indirizzo'],
+ 'numero_civico' => $data['numero_civico'],
+ 'cap' => $data['cap'],
+ 'telefono' => $data['telefono'],
+ 'email' => $data['email'],
+ 'luogo_dichiarazione' => $data['luogo_dichiarazione'],
+ 'data_dichiarazione' => $data['data_dichiarazione'],
+ 'bonifico_effettuato' => $data['bonifico_effettuato'],
+ 'consenso_statuto' => 1,
+ 'consenso_privacy' => 1,
+ 'consenso_newsletter' => $data['consenso_newsletter'],
+ 'registration_ip' => TON_Reg_Captcha::get_client_ip(),
+ 'user_agent' => self::user_agent(),
+ 'privacy_text_version' => $privacy,
+ 'consent_statuto_at' => $now,
+ 'consent_privacy_at' => $now,
+ 'status' => 'pending',
+ );
+
+ $reg_id = TON_Reg_Database::insert( $row );
+ if ( ! $reg_id ) {
+ wp_delete_user( $user_id );
+ self::redirect_error( $redirect, __( 'Errore nel salvataggio. Riprova.', 'ton-italia-registration' ) );
+ }
+
+ update_user_meta( $user_id, 'ton_reg_registration_id', $reg_id );
+ $data['registration_id'] = $reg_id;
+ TON_Reg_User_Manager::sync_user_meta( $user_id, $data );
+
+ $registration = TON_Reg_Database::get( $reg_id );
+ TON_Reg_Documents::ensure_member_folder( $registration );
+ TON_Reg_Mailer::send_admin_notification( $registration, $user_id );
+ TON_Reg_Mailer::send_user_confirmation( $registration, $user_id );
+
+ $success_url = add_query_arg(
+ array(
+ 'ton_reg' => 'success',
+ ),
+ $redirect
+ );
+ wp_safe_redirect( $success_url );
+ exit;
+ }
+
+ /**
+ * @param array $post Raw POST.
+ * @return array
+ */
+ private static function sanitize_post( $post ) {
+ $cf = isset( $post['codice_fiscale'] ) ? strtoupper( preg_replace( '/\s+/', '', sanitize_text_field( wp_unslash( $post['codice_fiscale'] ) ) ) ) : '';
+
+ return array(
+ 'cognome' => sanitize_text_field( wp_unslash( $post['cognome'] ?? '' ) ),
+ 'nome' => sanitize_text_field( wp_unslash( $post['nome'] ?? '' ) ),
+ 'luogo_nascita' => sanitize_text_field( wp_unslash( $post['luogo_nascita'] ?? '' ) ),
+ 'provincia_nascita' => strtoupper( sanitize_text_field( wp_unslash( $post['provincia_nascita'] ?? '' ) ) ),
+ 'data_nascita' => self::parse_date_input( sanitize_text_field( wp_unslash( $post['data_nascita'] ?? '' ) ) ),
+ 'codice_fiscale' => $cf,
+ 'comune_residenza' => sanitize_text_field( wp_unslash( $post['comune_residenza'] ?? '' ) ),
+ 'provincia_residenza' => strtoupper( sanitize_text_field( wp_unslash( $post['provincia_residenza'] ?? '' ) ) ),
+ 'indirizzo' => sanitize_text_field( wp_unslash( $post['indirizzo'] ?? '' ) ),
+ 'numero_civico' => sanitize_text_field( wp_unslash( $post['numero_civico'] ?? '' ) ),
+ 'cap' => sanitize_text_field( wp_unslash( $post['cap'] ?? '' ) ),
+ 'telefono' => sanitize_text_field( wp_unslash( $post['telefono'] ?? '' ) ),
+ 'email' => sanitize_email( wp_unslash( $post['email'] ?? '' ) ),
+ 'luogo_dichiarazione' => sanitize_text_field( wp_unslash( $post['luogo_dichiarazione'] ?? '' ) ),
+ 'data_dichiarazione' => self::parse_date_input( sanitize_text_field( wp_unslash( $post['data_dichiarazione'] ?? '' ) ) ),
+ 'bonifico_effettuato' => ! empty( $post['bonifico_effettuato'] ) ? 1 : 0,
+ 'consenso_statuto' => ! empty( $post['accettazione_statuto'] ) ? 1 : 0,
+ 'consenso_privacy' => ! empty( $post['consenso_privacy'] ) ? 1 : 0,
+ 'consenso_newsletter' => ! empty( $post['consenso_newsletter'] ) ? 1 : 0,
+ );
+ }
+
+ /**
+ * @param array $data Data.
+ * @return true|WP_Error
+ */
+ private static function validate( $data ) {
+ $required = array(
+ 'cognome' => __( 'Cognome', 'ton-italia-registration' ),
+ 'nome' => __( 'Nome', 'ton-italia-registration' ),
+ 'luogo_nascita' => __( 'Luogo di nascita', 'ton-italia-registration' ),
+ 'provincia_nascita' => __( 'Provincia di nascita', 'ton-italia-registration' ),
+ 'data_nascita' => __( 'Data di nascita', 'ton-italia-registration' ),
+ 'codice_fiscale' => __( 'Codice fiscale', 'ton-italia-registration' ),
+ 'comune_residenza' => __( 'Comune di residenza', 'ton-italia-registration' ),
+ 'provincia_residenza' => __( 'Provincia di residenza', 'ton-italia-registration' ),
+ 'indirizzo' => __( 'Indirizzo', 'ton-italia-registration' ),
+ 'numero_civico' => __( 'Numero civico', 'ton-italia-registration' ),
+ 'cap' => __( 'CAP', 'ton-italia-registration' ),
+ 'telefono' => __( 'Telefono', 'ton-italia-registration' ),
+ 'email' => __( 'Email', 'ton-italia-registration' ),
+ 'luogo_dichiarazione' => __( 'Luogo dichiarazione', 'ton-italia-registration' ),
+ 'data_dichiarazione' => __( 'Data dichiarazione', 'ton-italia-registration' ),
+ );
+
+ foreach ( $required as $key => $label ) {
+ if ( empty( $data[ $key ] ) ) {
+ return new WP_Error( 'required', sprintf( __( 'Il campo %s è obbligatorio.', 'ton-italia-registration' ), $label ) );
+ }
+ }
+
+ if ( ! is_email( $data['email'] ) ) {
+ return new WP_Error( 'email', __( 'Email non valida.', 'ton-italia-registration' ) );
+ }
+
+ if ( ! TON_Reg_User_Manager::validate_codice_fiscale( $data['codice_fiscale'] ) ) {
+ return new WP_Error( 'cf', __( 'Codice fiscale non valido.', 'ton-italia-registration' ) );
+ }
+
+ if ( ! preg_match( '/^\d{5}$/', $data['cap'] ) ) {
+ return new WP_Error( 'cap', __( 'CAP non valido.', 'ton-italia-registration' ) );
+ }
+
+ if ( ! preg_match( '/^[A-Z]{2}$/', $data['provincia_nascita'] ) || ! preg_match( '/^[A-Z]{2}$/', $data['provincia_residenza'] ) ) {
+ return new WP_Error( 'prov', __( 'Provincia non valida (2 lettere).', 'ton-italia-registration' ) );
+ }
+
+ if ( ! self::is_valid_date( $data['data_nascita'] ) || ! self::is_valid_date( $data['data_dichiarazione'] ) ) {
+ return new WP_Error( 'date', __( 'Data non valida. Usa il formato gg-mm-aaaa.', 'ton-italia-registration' ) );
+ }
+
+ if ( empty( $data['consenso_statuto'] ) ) {
+ return new WP_Error( 'statuto', __( 'Devi accettare la dichiarazione sullo statuto.', 'ton-italia-registration' ) );
+ }
+
+ if ( empty( $data['consenso_privacy'] ) ) {
+ return new WP_Error( 'privacy', __( 'Devi accettare l\'informativa privacy.', 'ton-italia-registration' ) );
+ }
+
+ if ( '1' === get_option( 'ton_reg_newsletter_required', '0' ) && empty( $data['consenso_newsletter'] ) ) {
+ return new WP_Error( 'newsletter', __( 'Devi accettare il consenso newsletter.', 'ton-italia-registration' ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse gg-mm-aaaa (or already normalized aaaa-mm-gg) to DB format Y-m-d.
+ *
+ * @param string $value User input.
+ * @return string Empty if invalid.
+ */
+ private static function parse_date_input( $value ) {
+ $value = trim( $value );
+ if ( preg_match( '/^(\d{2})-(\d{2})-(\d{4})$/', $value, $m ) ) {
+ $day = (int) $m[1];
+ $month = (int) $m[2];
+ $year = (int) $m[3];
+ if ( checkdate( $month, $day, $year ) ) {
+ return sprintf( '%04d-%02d-%02d', $year, $month, $day );
+ }
+ return '';
+ }
+ if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
+ $parts = array_map( 'intval', explode( '-', $value ) );
+ if ( checkdate( $parts[1], $parts[2], $parts[0] ) ) {
+ return $value;
+ }
+ }
+ return '';
+ }
+
+ /**
+ * @param string $date Date Y-m-d.
+ * @return bool
+ */
+ private static function is_valid_date( $date ) {
+ if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
+ return false;
+ }
+ $parts = array_map( 'intval', explode( '-', $date ) );
+ return checkdate( $parts[1], $parts[2], $parts[0] );
+ }
+
+ /**
+ * @return string
+ */
+ private static function user_agent() {
+ if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
+ return '';
+ }
+ return substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 255 );
+ }
+
+ /**
+ * @param string $url URL.
+ * @param string $message Message.
+ */
+ private static function redirect_error( $url, $message ) {
+ $url = add_query_arg(
+ array(
+ 'ton_reg' => 'error',
+ 'ton_msg' => rawurlencode( $message ),
+ ),
+ $url
+ );
+ wp_safe_redirect( $url );
+ exit;
+ }
+}
diff --git a/includes/class-uninstaller.php b/includes/class-uninstaller.php
new file mode 100644
index 0000000..1201be5
--- /dev/null
+++ b/includes/class-uninstaller.php
@@ -0,0 +1,65 @@
+prefix . TON_REG_TABLE;
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $registrations = $wpdb->get_results( "SELECT id FROM {$table}" );
+ if ( $registrations ) {
+ foreach ( $registrations as $reg ) {
+ TON_Reg_Documents::delete_all_for_registration( (int) $reg->id, false );
+ }
+ }
+ TON_Reg_Documents::uninstall_folders();
+
+ $delete_users = '1' === get_option( 'ton_reg_delete_users_on_uninstall', '0' );
+
+ if ( $delete_users ) {
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $user_ids = $wpdb->get_col( "SELECT user_id FROM {$table} WHERE user_id IS NOT NULL" );
+ foreach ( $user_ids as $user_id ) {
+ if ( $user_id ) {
+ wp_delete_user( (int) $user_id );
+ }
+ }
+ }
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . TON_REG_TABLE );
+
+ $options = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
+ 'ton_reg_%'
+ )
+ );
+
+ foreach ( $options as $option ) {
+ delete_option( $option );
+ }
+
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
+ '_transient_ton_reg_%',
+ '_transient_timeout_ton_reg_%'
+ )
+ );
+ }
+}
diff --git a/includes/class-user-manager.php b/includes/class-user-manager.php
new file mode 100644
index 0000000..50bf25b
--- /dev/null
+++ b/includes/class-user-manager.php
@@ -0,0 +1,118 @@
+ $data Registration data.
+ * @return int|WP_Error User ID.
+ */
+ public static function create_subscriber( $data ) {
+ $email = sanitize_email( $data['email'] );
+ if ( email_exists( $email ) ) {
+ return new WP_Error( 'ton_reg_email_exists', __( 'Questa email è già registrata.', 'ton-italia-registration' ) );
+ }
+
+ $password = wp_generate_password( 24, true, true );
+ $login = self::unique_login_from_email( $email );
+
+ $user_id = wp_insert_user(
+ array(
+ 'user_login' => $login,
+ 'user_email' => $email,
+ 'user_pass' => $password,
+ 'first_name' => sanitize_text_field( $data['nome'] ),
+ 'last_name' => sanitize_text_field( $data['cognome'] ),
+ 'display_name' => sanitize_text_field( $data['nome'] . ' ' . $data['cognome'] ),
+ 'role' => 'subscriber',
+ )
+ );
+
+ if ( is_wp_error( $user_id ) ) {
+ return $user_id;
+ }
+
+ self::sync_user_meta( (int) $user_id, $data );
+
+ return (int) $user_id;
+ }
+
+ /**
+ * @param string $email Email.
+ * @return string
+ */
+ private static function unique_login_from_email( $email ) {
+ $base = sanitize_user( current( explode( '@', $email ) ), true );
+ if ( '' === $base ) {
+ $base = 'socio';
+ }
+ $login = $base;
+ $i = 1;
+ while ( username_exists( $login ) ) {
+ $login = $base . $i;
+ ++$i;
+ }
+ return $login;
+ }
+
+ /**
+ * @param int $user_id User ID.
+ * @param array $data Data.
+ */
+ public static function sync_user_meta( $user_id, $data ) {
+ $keys = array(
+ 'cognome',
+ 'nome',
+ 'luogo_nascita',
+ 'provincia_nascita',
+ 'data_nascita',
+ 'codice_fiscale',
+ 'comune_residenza',
+ 'provincia_residenza',
+ 'indirizzo',
+ 'numero_civico',
+ 'cap',
+ 'telefono',
+ );
+ foreach ( $keys as $key ) {
+ if ( isset( $data[ $key ] ) ) {
+ update_user_meta( $user_id, 'ton_reg_' . $key, $data[ $key ] );
+ }
+ }
+ update_user_meta( $user_id, 'ton_reg_registration_id', isset( $data['registration_id'] ) ? (int) $data['registration_id'] : 0 );
+ }
+
+ /**
+ * @param string $value Codice fiscale.
+ * @return bool
+ */
+ public static function validate_codice_fiscale( $value ) {
+ $value = strtoupper( preg_replace( '/\s+/', '', $value ) );
+ return (bool) preg_match( '/^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/', $value );
+ }
+
+ /**
+ * @param string $date Date Y-m-d.
+ * @return bool
+ */
+ public static function is_minor( $date ) {
+ if ( ! $date ) {
+ return false;
+ }
+ $birth = strtotime( $date );
+ if ( ! $birth ) {
+ return false;
+ }
+ $eighteen = strtotime( '-18 years' );
+ return $birth > $eighteen;
+ }
+}
diff --git a/languages/ton-italia-registration-it_IT.po b/languages/ton-italia-registration-it_IT.po
new file mode 100644
index 0000000..995e66e
--- /dev/null
+++ b/languages/ton-italia-registration-it_IT.po
@@ -0,0 +1,17 @@
+# Italian translations for TON Italia Registration
+msgid ""
+msgstr ""
+"Project-Id-Version: TON Italia Registration 1.5\n"
+"Language: it_IT\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Domanda di ammissione a socio e contestuale tesseramento"
+msgstr "Domanda di ammissione a socio e contestuale tesseramento"
+
+msgid "Dati anagrafici"
+msgstr "Dati anagrafici"
+
+msgid "Invia domanda di iscrizione"
+msgstr "Invia domanda di iscrizione"
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 0000000..4c40cee
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,81 @@
+=== TON Italia Registration ===
+Contributors: fabioarrigoni
+Tags: registration, membership, gdpr, form
+Requires at least: 6.0
+Tested up to: 6.7
+Requires PHP: 8.0
+Stable tag: 1.5
+License: GPLv2 or later
+
+Form di iscrizione socio per TON ITALIA ODV con gestione ammissioni, GDPR e CAPTCHA integrato.
+
+== Description ==
+
+Shortcode: `[ton_registration_form]`
+
+Funzionalità principali:
+
+* Form pubblico con campi del modulo PDF «Domanda di ammissione a socio e contestuale tesseramento»
+* Due checkbox al posto delle firme (statuto e privacy)
+* Testi privacy, dichiarazioni e istruzioni bonifico editabili in admin
+* CAPTCHA integrato (honeypot, verifica matematica, rate limit per IP)
+* Salvataggio in tabella dedicata `wp_ton_registrations` con IP e timestamp consensi
+* Creazione utente WordPress con ruolo Subscriber (senza accesso a wp-admin)
+* Email di notifica admin e conferma utente
+* Pannello admin: elenco, dettaglio, stato Ammesso / Non ammesso, data Libro Associati
+* Strumenti GDPR: export JSON, anonimizzazione, eliminazione definitiva
+
+== Installation ==
+
+1. Caricare la cartella `ton-italia-registration` in `wp-content/plugins/`
+2. Attivare il plugin da Plugin → Plugin installati
+3. Inserire lo shortcode `[ton_registration_form]` in una pagina
+4. Configurare testi e email in **TON Iscrizioni → Impostazioni**
+
+== GDPR ==
+
+* L'IP del richiedente viene registrato per finalità legali e di sicurezza
+* Versione informativa privacy salvata al momento del consenso
+* Export, anonimizzazione ed eliminazione disponibili dalla scheda singola iscrizione
+* Contatto GDPR predefinito: qeshet.tonitalia@gmail.com (modificabile in impostazioni)
+
+== Uninstall ==
+
+Alla **disinstallazione** (eliminazione plugin, non disattivazione):
+
+* Viene eliminata la tabella `wp_ton_registrations`
+* Vengono rimosse tutte le opzioni `ton_reg_*` e i transient correlati
+* Gli utenti Subscriber creati dal plugin **non** vengono eliminati automaticamente (impostazione predefinita)
+
+Per eliminare anche gli utenti alla disinstallazione, attivare l'opzione corrispondente in Impostazioni prima di rimuovere il plugin.
+
+== Frequently Asked Questions ==
+
+= Quale ruolo WordPress viene assegnato? =
+
+Subscriber, senza accesso all'area amministrativa.
+
+= Serve reCAPTCHA o servizi esterni? =
+
+No. Il CAPTCHA è integrato nel plugin.
+
+== Changelog ==
+
+= 1.5 =
+* Aggiunto caricamento documento «Ricevuta pagamento» in admin
+
+= 1.4 =
+* Dettaglio iscrizione: sezione esplicita consensi (statuto, privacy, newsletter) con data/ora
+
+= 1.3 =
+* Corretto elenco iscrizioni in admin (paginazione e visualizzazione tabella)
+
+= 1.2 =
+* Date del form in formato gg-mm-aaaa (nascita e dichiarazione)
+
+= 1.1 =
+* Caricamento documento identità e ricevuta bonifico in admin (cartella Media «Documenti Iscritti»)
+* Miglioramenti form: ordine sezioni, placeholder, validazione client/server
+
+= 1.0 =
+* Release iniziale
diff --git a/ton-italia-registration.php b/ton-italia-registration.php
new file mode 100644
index 0000000..d46e7a0
--- /dev/null
+++ b/ton-italia-registration.php
@@ -0,0 +1,37 @@
+