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; + } + ?> +
+

+

+ + + +

+

+ +

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
$field ?? '' ); ?>
+ +
+ +
+ +

+

+ + + anonymized_at ) : ?> + + + +

+
+ +

+

+ + + + + + + + + + + + + + + + + + + + + +
+ consenso_statuto ) ); ?> + consent_statuto_at ) ) : ?> +

consent_statuto_at ) ); ?>

+ +
+ consenso_privacy ) ); ?> + consent_privacy_at ) ) : ?> +

consent_privacy_at ) ); ?>

+ + privacy_text_version ) ) : ?> +

+ privacy_text_version ) + ); + ?> +

+ +
+ consenso_newsletter ) ) : ?> + + + + +
+ bonifico_effettuato ), false ); ?> +
+ luogo_dichiarazione ?? '' ) . ( ! empty( $row->data_dichiarazione ) ? ', ' . self::format_date_display( $row->data_dichiarazione ) : '' ) + ) + ); + ?> +
+ ' . 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 '

' . esc_html( $msg ) . '

'; + } + } + + /** + * @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' ), + ); + ?> + + + + + + + + + + + + + $label ) : ?> + + + + + + + +
IBAN
/>
/>
/>
+

+ +
+
+ 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 '

' . esc_html( $msg ) . '

'; + } + + /** + * @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 @@ +