diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..963081e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+Thumbs.db
+*.bak
+*.bak-*
+*.zip
+*.log
+node_modules/
+vendor/
diff --git a/assets/css/admin-styles.css b/assets/css/admin-styles.css
new file mode 100644
index 0000000..89f355e
--- /dev/null
+++ b/assets/css/admin-styles.css
@@ -0,0 +1,26 @@
+.ojs-field-wrapper {
+ margin-bottom: 12px;
+}
+
+.ojs-field-wrapper label {
+ font-weight: 600;
+}
+
+.ojs-field-wrapper .description {
+ color: #d63638;
+ font-weight: bold;
+}
+
+#ojs-subscription-fields {
+ border-left: 3px solid #2271b1;
+ padding-left: 16px;
+ margin: 12px 0;
+}
+
+#ojs-subscribe-toggle {
+ margin: 16px 0;
+ padding: 12px;
+ background: #f0f6fc;
+ border: 1px solid #c3c4c7;
+ border-radius: 4px;
+}
diff --git a/assets/js/conditional-fields.js b/assets/js/conditional-fields.js
new file mode 100644
index 0000000..a2a1e47
--- /dev/null
+++ b/assets/js/conditional-fields.js
@@ -0,0 +1,69 @@
+(function() {
+ 'use strict';
+
+ document.addEventListener('DOMContentLoaded', function() {
+ initSubscriptionToggle();
+ initMembershipTypeToggle();
+ initDeactivationConfirm();
+ });
+
+ function initSubscriptionToggle() {
+ var checkbox = document.getElementById('ojs_subscription_requested');
+ var fields = document.getElementById('ojs-subscription-fields');
+ if (!checkbox || !fields) return;
+
+ function toggle() {
+ fields.style.display = checkbox.checked ? '' : 'none';
+ var required = fields.querySelectorAll('[data-conditional-required]');
+ for (var i = 0; i < required.length; i++) {
+ required[i].required = checkbox.checked;
+ }
+ }
+
+ checkbox.addEventListener('change', toggle);
+ toggle();
+ }
+
+ function initMembershipTypeToggle() {
+ var radios = document.querySelectorAll('input[name="ojs_membership_type"]');
+ if (!radios.length) return;
+
+ var wrapper = document.getElementById('ojs-field-ojs_student_document');
+ if (!wrapper) return;
+
+ function toggle() {
+ var checked = document.querySelector('input[name="ojs_membership_type"]:checked');
+ var isStudent = checked && (checked.value === 'student' || checked.value === 'student_junior');
+ wrapper.style.display = isStudent ? '' : 'none';
+ var fileInput = wrapper.querySelector('input[type="file"]');
+ if (fileInput) {
+ fileInput.required = isStudent;
+ }
+ }
+
+ for (var i = 0; i < radios.length; i++) {
+ radios[i].addEventListener('change', toggle);
+ }
+ toggle();
+ }
+
+ function initDeactivationConfirm() {
+ if (typeof wpOjsSsoBridge === 'undefined') return;
+
+ var checkbox = document.querySelector('input[id$="subscription_active"][type="checkbox"]');
+ if (!checkbox) return;
+
+ var wasChecked = checkbox.checked;
+ var form = checkbox.closest('form');
+ if (!form) return;
+
+ form.addEventListener('submit', function(e) {
+ if (wasChecked && !checkbox.checked) {
+ if (!confirm(wpOjsSsoBridge.confirmDeactivation)) {
+ checkbox.checked = true;
+ e.preventDefault();
+ }
+ }
+ });
+ }
+})();
diff --git a/includes/class-file-upload.php b/includes/class-file-upload.php
new file mode 100644
index 0000000..cff67a7
--- /dev/null
+++ b/includes/class-file-upload.php
@@ -0,0 +1,94 @@
+ 0 ) {
+ $p = get_attached_file( (int) $val );
+ if ( $p && is_readable( $p ) ) {
+ $path = $p;
+ }
+ } elseif ( $val && is_string( $val ) && is_file( $val ) && is_readable( $val ) ) {
+ $path = $val;
+ }
+
+ if ( ! $path ) {
+ wp_die( esc_html__( 'File not found', 'wp-ojs-sso-bridge' ), 404 );
+ }
+
+ $finfo = finfo_open( FILEINFO_MIME_TYPE );
+ $mime = finfo_file( $finfo, $path );
+ finfo_close( $finfo );
+
+ header( 'Content-Type: ' . $mime );
+ header( 'Content-Disposition: attachment; filename="' . rawurlencode( basename( $path ) ) . '"' );
+ header( 'Content-Length: ' . filesize( $path ) );
+ readfile( $path );
+ exit;
+ }
+
+ public static function get_admin_download_url( $user_id ) {
+ return wp_nonce_url(
+ admin_url( 'admin-post.php?action=ojs_download_document&user_id=' . (int) $user_id ),
+ 'ojs_download_doc_' . (int) $user_id
+ );
+ }
+
+ public static function delete_user_files( $user_id ) {
+ if ( class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ WP_OJS_SSO_Subscription_Media::on_delete_user( $user_id );
+ return;
+ }
+ $dir = self::get_upload_dir( $user_id );
+ if ( is_dir( $dir ) ) {
+ $files = glob( $dir . '/*' );
+ if ( $files ) {
+ array_map( 'unlink', $files );
+ }
+ @rmdir( $dir );
+ }
+ }
+}
diff --git a/includes/class-oidc-bridge.php b/includes/class-oidc-bridge.php
new file mode 100644
index 0000000..2c8b6d8
--- /dev/null
+++ b/includes/class-oidc-bridge.php
@@ -0,0 +1,82 @@
+ 'OJS Journal',
+ 'secret' => $opts['client_secret'] ?? '',
+ 'redirect_uri' => $opts['redirect_uri'] ?? '',
+ 'grant_types' => [ 'authorization_code' ],
+ 'scope' => 'openid profile email',
+ ];
+
+ return $clients;
+ }
+
+ public function add_claims( $claims, $user ) {
+ $opts = WP_OJS_SSO_Plugin::get_option();
+ $meta_key = $opts['user_meta_key'] ?? 'ojs_subscription_active';
+ $claim_name = $opts['claim_name'] ?? 'subscription_active';
+
+ if ( ! empty( $user->user_email ) ) {
+ $claims['email'] = $user->user_email;
+ $claims['email_verified'] = true;
+ }
+
+ if ( ! empty( $user->user_login ) && empty( $claims['preferred_username'] ) ) {
+ $claims['preferred_username'] = $user->user_login;
+ }
+
+ if ( empty( $claims['given_name'] ) && ! empty( $user->first_name ) ) {
+ $claims['given_name'] = $user->first_name;
+ }
+ if ( empty( $claims['family_name'] ) && ! empty( $user->last_name ) ) {
+ $claims['family_name'] = $user->last_name;
+ }
+
+ if ( ! empty( $claim_name ) && preg_match( '/^[a-z_][a-z0-9_]*$/', $claim_name ) ) {
+ $claims[ $claim_name ] = (bool) get_user_meta( $user->ID, $meta_key, true );
+ }
+
+ return $claims;
+ }
+}
diff --git a/includes/class-oidc-server-it-locale.php b/includes/class-oidc-server-it-locale.php
new file mode 100644
index 0000000..2182493
--- /dev/null
+++ b/includes/class-oidc-server-it-locale.php
@@ -0,0 +1,79 @@
+ 'Ciao %s!',
+ 'Do you want to log in to %1$s with your %2$s account?'
+ => 'Vuoi accedere a %1$s usando il tuo account %2$s ?',
+ 'Authorize' => 'Autorizza',
+ 'Cancel' => 'Annulla',
+ "You don't have permission to use OpenID Connect."
+ => "Non hai l'autorizzazione per usare OpenID Connect.",
+ 'Contact your administrator for more details.'
+ => "Contatta l'amministratore del sito per maggiori informazioni.",
+ ];
+ }
+ return $oidc[ $text ] ?? $translation;
+ }
+
+ if ( 'default' === $domain ) {
+ static $core = null;
+ if ( null === $core ) {
+ $core = [
+ 'Registration confirmation will be emailed to you.'
+ => "Riceverai un'email di conferma della registrazione.",
+ ];
+ }
+ if ( isset( $core[ $text ] ) && ( $translation === $text || '' === $translation ) ) {
+ return $core[ $text ];
+ }
+ return $translation;
+ }
+
+ return $translation;
+ }
+
+ /**
+ * The OIDC server calls login_header( 'OIDC Connect' ) — that string is not i18n.
+ *
+ * @param string $login_title The title to show in the <title> and heading.
+ * @param string $title The raw first argument to login_header().
+ */
+ public static function italian_login_title( $login_title, $title ) {
+ if ( 'OIDC Connect' === $title ) {
+ return 'Accesso con OpenID Connect';
+ }
+ return $login_title;
+ }
+}
diff --git a/includes/class-plugin.php b/includes/class-plugin.php
new file mode 100644
index 0000000..75b0500
--- /dev/null
+++ b/includes/class-plugin.php
@@ -0,0 +1,108 @@
+load_dependencies();
+ add_action( 'plugins_loaded', [ $this, 'init' ] );
+ }
+
+ private function load_dependencies() {
+ require_once WP_OJS_SSO_DIR . 'includes/class-settings.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-user-meta.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-subscription-fields.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-user-subscription-media.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-file-upload.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-registration.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-profile.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-oidc-bridge.php';
+ require_once WP_OJS_SSO_DIR . 'includes/class-oidc-server-it-locale.php';
+ }
+
+ public function init() {
+ load_plugin_textdomain( 'wp-ojs-sso-bridge', false, dirname( WP_OJS_SSO_BASENAME ) . '/languages' );
+ self::maybe_load_translations_from_po();
+
+ WP_OJS_SSO_OIDC_Server_It_Locale::init();
+
+ WP_OJS_SSO_Settings::instance();
+ WP_OJS_SSO_User_Meta::instance();
+ WP_OJS_SSO_Subscription_Fields::instance();
+ WP_OJS_SSO_File_Upload::instance();
+ WP_OJS_SSO_Registration::instance();
+ WP_OJS_SSO_Profile::instance();
+ WP_OJS_SSO_OIDC_Bridge::instance();
+
+ add_action( 'delete_user', [ 'WP_OJS_SSO_Subscription_Media', 'on_delete_user' ] );
+
+ add_action( 'admin_notices', [ $this, 'dependency_notice' ] );
+ }
+
+ /**
+ * WordPress only loads .mo files; without a compiled catalog, strings stay in English.
+ * For Italian locales, load the shipped .po via core POMO when no .mo is present.
+ */
+ private static function maybe_load_translations_from_po() {
+ if ( 0 !== strpos( get_locale(), 'it' ) ) {
+ return;
+ }
+ if ( is_textdomain_loaded( 'wp-ojs-sso-bridge' ) ) {
+ return;
+ }
+ $pofile = WP_OJS_SSO_DIR . 'languages/wp-ojs-sso-bridge-it_IT.po';
+ if ( ! is_readable( $pofile ) ) {
+ return;
+ }
+ require_once ABSPATH . 'wp-includes/pomo/po.php';
+ $po = new PO();
+ if ( ! $po->import_from_file( $pofile ) ) {
+ return;
+ }
+ $GLOBALS['l10n']['wp-ojs-sso-bridge'] = $po;
+ }
+
+ public function dependency_notice() {
+ if ( ! is_plugin_active( 'openid-connect-server/openid-connect-server.php' ) ) {
+ printf(
+ '
',
+ esc_html__( 'WP OJS SSO Bridge requires the "OpenID Connect Server" plugin by Automattic for SSO functionality.', 'wp-ojs-sso-bridge' )
+ );
+ }
+ }
+
+ public static function get_option( $key = null, $default = null ) {
+ $opts = get_option( 'wp_ojs_sso_bridge_settings', [] );
+ if ( null === $key ) {
+ return $opts;
+ }
+ return $opts[ $key ] ?? $default;
+ }
+
+ public static function activate() {
+ flush_rewrite_rules();
+ $upload_dir = wp_upload_dir();
+ $plugin_upload = $upload_dir['basedir'] . '/ojs-sso-bridge';
+ if ( ! is_dir( $plugin_upload ) ) {
+ wp_mkdir_p( $plugin_upload );
+ file_put_contents( $plugin_upload . '/.htaccess', "Require all denied\n" );
+ file_put_contents( $plugin_upload . '/index.php', " esc_html__(
+ 'Deactivating the subscription will immediately disconnect this user from WordPress and revoke their access to the journal on OJS. Continue?',
+ 'wp-ojs-sso-bridge'
+ ),
+ ] );
+
+ // Ensure profile form supports file uploads
+ add_action( 'admin_footer', function() {
+ echo '';
+ } );
+ }
+
+ /**
+ * Render subscription fields on the user's own profile page.
+ * Users can request subscription or edit their own data, but cannot toggle active status.
+ */
+ public function render_user_fields( $user ) {
+ $requested = (bool) get_user_meta( $user->ID, 'ojs_subscription_requested', true );
+ $meta_key = WP_OJS_SSO_Plugin::get_option( 'user_meta_key', 'ojs_subscription_active' );
+ $active = (bool) get_user_meta( $user->ID, $meta_key, true );
+
+ if ( ! $active && ! $requested && ! WP_OJS_SSO_Plugin::get_option( 'enable_registration_checkbox' ) ) {
+ return; // subscription request not enabled and user hasn't requested
+ }
+
+ echo '' . esc_html__( 'Journal Subscription', 'wp-ojs-sso-bridge' ) . ' ';
+ echo '';
+ echo '';
+ echo '';
+ }
+
+ /**
+ * Render fields for admin editing another user.
+ * Admin can see all data, toggle active status, and download documents.
+ */
+ public function render_admin_fields( $user ) {
+ $meta_key = WP_OJS_SSO_Plugin::get_option( 'user_meta_key', 'ojs_subscription_active' );
+ $active = (bool) get_user_meta( $user->ID, $meta_key, true );
+ $requested = (bool) get_user_meta( $user->ID, 'ojs_subscription_requested', true );
+
+ echo '' . esc_html__( 'Journal Subscription', 'wp-ojs-sso-bridge' ) . ' ';
+ echo '';
+ }
+
+ private function render_subscription_data_rows( $user, $is_admin ) {
+ $fields = WP_OJS_SSO_Subscription_Fields::get_fields();
+
+ // First/last name
+ echo '' . esc_html__( 'First Name', 'wp-ojs-sso-bridge' ) . ' * ';
+ printf( ' ', esc_attr( $user->first_name ) );
+ echo ' ';
+
+ echo '' . esc_html__( 'Last Name', 'wp-ojs-sso-bridge' ) . ' * ';
+ printf( ' ', esc_attr( $user->last_name ) );
+ echo ' ';
+
+ foreach ( $fields as $key => $field ) {
+ $mkey = $field['meta_key'] ?? $key;
+ $value = get_user_meta( $user->ID, $mkey, true );
+
+ echo '';
+ echo '' . esc_html( $field['label'] );
+ if ( ! empty( $field['required'] ) ) {
+ echo ' *';
+ }
+ echo ' ';
+
+ if ( 'file' === $field['type'] ) {
+ $show_name = '';
+ $has_file = false;
+ if ( 'ojs_bonifico_receipt' === $key && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ if ( WP_OJS_SSO_Subscription_Media::get_attachment_id( $user->ID, WP_OJS_SSO_Subscription_Media::KIND_BONIFICO ) > 0 ) {
+ $has_file = true;
+ $show_name = WP_OJS_SSO_Subscription_Media::get_file_label( $user->ID, WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ }
+ } elseif ( 'ojs_student_document' === $key && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ if ( WP_OJS_SSO_Subscription_Media::get_attachment_id( $user->ID, WP_OJS_SSO_Subscription_Media::KIND_CERTIFICATO ) > 0 ) {
+ $has_file = true;
+ $show_name = WP_OJS_SSO_Subscription_Media::get_file_label( $user->ID, WP_OJS_SSO_Subscription_Media::KIND_CERTIFICATO );
+ } elseif ( is_string( $value ) && $value && is_file( $value ) ) {
+ $has_file = true;
+ $show_name = wp_basename( $value );
+ }
+ }
+
+ if ( $has_file && $show_name ) {
+ echo '' . esc_html( $show_name );
+ if ( $is_admin && 'ojs_student_document' === $key && is_string( $value ) && $value && is_file( $value ) && ! is_numeric( $value ) ) {
+ $url = WP_OJS_SSO_File_Upload::get_admin_download_url( $user->ID );
+ echo ' — ' . esc_html__( 'Download', 'wp-ojs-sso-bridge' ) . ' ';
+ } elseif ( $is_admin && ( 'ojs_bonifico_receipt' === $key || ( 'ojs_student_document' === $key && is_numeric( $value ) ) ) ) {
+ $kind = ( 'ojs_bonifico_receipt' === $key ) ? WP_OJS_SSO_Subscription_Media::KIND_BONIFICO : WP_OJS_SSO_Subscription_Media::KIND_CERTIFICATO;
+ $elink = WP_OJS_SSO_Subscription_Media::get_edit_link( $user->ID, $kind );
+ if ( $elink ) {
+ echo ' — ' . esc_html__( 'Open in Media', 'wp-ojs-sso-bridge' ) . ' ';
+ }
+ } elseif ( ! $is_admin && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ $aid = 0;
+ if ( 'ojs_bonifico_receipt' === $key ) {
+ $aid = WP_OJS_SSO_Subscription_Media::get_attachment_id( $user->ID, WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ } elseif ( 'ojs_student_document' === $key && is_numeric( $value ) ) {
+ $aid = (int) $value;
+ }
+ if ( $aid > 0 ) {
+ $u = wp_get_attachment_url( $aid );
+ if ( $u ) {
+ echo ' — ' . esc_html__( 'View file', 'wp-ojs-sso-bridge' ) . ' ';
+ }
+ }
+ }
+ echo '
';
+ }
+ printf(
+ ' ',
+ esc_attr( $key ),
+ esc_attr( $field['accept'] ?? '' )
+ );
+ echo '' . esc_html__( 'Upload a new file to replace the existing one.', 'wp-ojs-sso-bridge' ) . '
';
+ } else {
+ WP_OJS_SSO_Subscription_Fields::render_field( $key, $field, $value );
+ }
+
+ echo ' ';
+ }
+ }
+
+ public function save_user_fields( $user_id ) {
+ if ( ! wp_verify_nonce( $_POST['_wp_ojs_sso_profile_nonce'] ?? '', 'wp_ojs_sso_save_profile' ) ) {
+ return;
+ }
+
+ // User requesting subscription from profile
+ if ( ! empty( $_POST['ojs_subscription_requested'] ) ) {
+ $was_requested = (bool) get_user_meta( $user_id, 'ojs_subscription_requested', true );
+ update_user_meta( $user_id, 'ojs_subscription_requested', '1' );
+
+ $errors = WP_OJS_SSO_Subscription_Fields::validate( $_POST, $_FILES, $user_id );
+ if ( empty( $errors ) ) {
+ WP_OJS_SSO_Subscription_Fields::save_fields( $user_id, $_POST );
+
+ if ( ! empty( $_FILES['ojs_student_document']['name'] ) ) {
+ WP_OJS_SSO_File_Upload::handle_upload( $user_id );
+ }
+ if ( ! empty( $_FILES['ojs_bonifico_receipt']['name'] ) && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ WP_OJS_SSO_Subscription_Media::handle_upload( $user_id, 'ojs_bonifico_receipt', WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ }
+
+ // Notify admin for new request
+ if ( ! $was_requested && WP_OJS_SSO_Plugin::get_option( 'notify_admin_on_request' ) ) {
+ $user = get_userdata( $user_id );
+ wp_mail(
+ get_option( 'admin_email' ),
+ sprintf( __( '[%s] New journal subscription request', 'wp-ojs-sso-bridge' ), get_bloginfo( 'name' ) ),
+ sprintf(
+ __( "User \"%1\$s\" (%2\$s) has requested a journal subscription from their profile.\n\nReview: %3\$s", 'wp-ojs-sso-bridge' ),
+ $user->user_login,
+ $user->user_email,
+ admin_url( 'users.php?ojs_sub_filter=requested' )
+ )
+ );
+ }
+ }
+ }
+
+ // If already subscribed, allow editing fields
+ $meta_key = WP_OJS_SSO_Plugin::get_option( 'user_meta_key', 'ojs_subscription_active' );
+ if ( (bool) get_user_meta( $user_id, $meta_key, true ) ) {
+ WP_OJS_SSO_Subscription_Fields::save_fields( $user_id, $_POST );
+ if ( ! empty( $_FILES['ojs_student_document']['name'] ) ) {
+ WP_OJS_SSO_File_Upload::handle_upload( $user_id );
+ }
+ if ( ! empty( $_FILES['ojs_bonifico_receipt']['name'] ) && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ WP_OJS_SSO_Subscription_Media::handle_upload( $user_id, 'ojs_bonifico_receipt', WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ }
+ }
+ }
+
+ public function save_admin_fields( $user_id ) {
+ if ( ! current_user_can( 'edit_users' ) ) {
+ return;
+ }
+ if ( ! wp_verify_nonce( $_POST['_wp_ojs_sso_nonce'] ?? '', 'wp_ojs_sso_save_meta' ) ) {
+ return;
+ }
+
+ $meta_key = WP_OJS_SSO_Plugin::get_option( 'user_meta_key', 'ojs_subscription_active' );
+ $old_value = (bool) get_user_meta( $user_id, $meta_key, true );
+ $new_value = ! empty( $_POST[ $meta_key ] );
+
+ update_user_meta( $user_id, $meta_key, $new_value ? '1' : '' );
+
+ // Session invalidation on deactivation
+ if ( $old_value && ! $new_value ) {
+ $sessions = WP_Session_Tokens::get_instance( $user_id );
+ $sessions->destroy_all();
+ }
+
+ do_action( 'wp_ojs_sso_subscription_changed', $user_id, $new_value );
+
+ // Save subscription data fields
+ WP_OJS_SSO_Subscription_Fields::save_fields( $user_id, $_POST );
+
+ if ( ! empty( $_FILES['ojs_student_document']['name'] ) ) {
+ WP_OJS_SSO_File_Upload::handle_upload( $user_id );
+ }
+ if ( ! empty( $_FILES['ojs_bonifico_receipt']['name'] ) && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ WP_OJS_SSO_Subscription_Media::handle_upload( $user_id, 'ojs_bonifico_receipt', WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ }
+ }
+}
diff --git a/includes/class-registration.php b/includes/class-registration.php
new file mode 100644
index 0000000..263917a
--- /dev/null
+++ b/includes/class-registration.php
@@ -0,0 +1,155 @@
+document.getElementById("registerform").enctype="multipart/form-data";';
+
+ $checkbox_text = WP_OJS_SSO_Plugin::get_option(
+ 'registration_checkbox_text',
+ __( 'I want to subscribe to the journal', 'wp-ojs-sso-bridge' )
+ );
+ $description = WP_OJS_SSO_Plugin::get_option(
+ 'registration_description',
+ __( 'Your subscription will be activated after verification by an administrator.', 'wp-ojs-sso-bridge' )
+ );
+
+ $requested = ! empty( $_POST['ojs_subscription_requested'] );
+ ?>
+
+
+ />
+
+
+
+
+
+
+
+
+
+ *
+
+
+
+ *
+
+
+
+ $field ) {
+ $value = sanitize_text_field( $_POST[ $key ] ?? '' );
+ echo '
';
+ WP_OJS_SSO_Subscription_Fields::render_field( $key, $field, $value );
+ echo '
';
+ }
+ ?>
+
+ add( 'first_name_error', __( 'Error : First Name is required for subscription.', 'wp-ojs-sso-bridge' ) );
+ }
+ if ( empty( $_POST['last_name'] ) ) {
+ $errors->add( 'last_name_error', __( 'Error : Last Name is required for subscription.', 'wp-ojs-sso-bridge' ) );
+ }
+
+ $field_errors = WP_OJS_SSO_Subscription_Fields::validate( $_POST, $_FILES );
+ foreach ( $field_errors as $code => $msg ) {
+ $errors->add( $code . '_error', '' . esc_html__( 'Error', 'wp-ojs-sso-bridge' ) . ' : ' . esc_html( $msg ) );
+ }
+
+ return $errors;
+ }
+
+ public function save( $user_id ) {
+ if ( empty( $_POST['ojs_subscription_requested'] ) ) {
+ return;
+ }
+
+ update_user_meta( $user_id, 'ojs_subscription_requested', '1' );
+ WP_OJS_SSO_Subscription_Fields::save_fields( $user_id, $_POST );
+
+ // Media uploads (certificato + bonifico)
+ if ( ! empty( $_FILES['ojs_student_document']['name'] ) ) {
+ WP_OJS_SSO_File_Upload::handle_upload( $user_id );
+ }
+ if ( ! empty( $_FILES['ojs_bonifico_receipt']['name'] ) && class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ WP_OJS_SSO_Subscription_Media::handle_upload( $user_id, 'ojs_bonifico_receipt', WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ }
+
+ // Notify admin
+ if ( WP_OJS_SSO_Plugin::get_option( 'notify_admin_on_request' ) ) {
+ $user = get_userdata( $user_id );
+ $admin_email = get_option( 'admin_email' );
+ $subject = sprintf(
+ /* translators: %s: site name */
+ __( '[%s] New journal subscription request', 'wp-ojs-sso-bridge' ),
+ get_bloginfo( 'name' )
+ );
+ $body = sprintf(
+ /* translators: 1: username, 2: email, 3: admin users URL */
+ __( "User \"%1\$s\" (%2\$s) has requested a journal subscription.\n\nReview pending requests: %3\$s", 'wp-ojs-sso-bridge' ),
+ $user->user_login,
+ $user->user_email,
+ admin_url( 'users.php?ojs_sub_filter=requested' )
+ );
+ wp_mail( $admin_email, $subject, $body );
+ }
+ }
+}
diff --git a/includes/class-settings.php b/includes/class-settings.php
new file mode 100644
index 0000000..00c04f0
--- /dev/null
+++ b/includes/class-settings.php
@@ -0,0 +1,184 @@
+%s', esc_url( $url ), esc_html__( 'Settings', 'wp-ojs-sso-bridge' ) ) );
+ return $links;
+ }
+
+ public function register_settings() {
+ register_setting( 'wp_ojs_sso_bridge', 'wp_ojs_sso_bridge_settings', [
+ 'sanitize_callback' => [ $this, 'sanitize' ],
+ ] );
+
+ // --- OIDC Section ---
+ add_settings_section( 'oidc', __( 'OpenID Connect', 'wp-ojs-sso-bridge' ), '__return_false', 'wp-ojs-sso-bridge' );
+
+ $this->add_field( 'client_id', __( 'OJS Client ID', 'wp-ojs-sso-bridge' ), 'text', 'oidc', 'ojs-arstexnica' );
+ $this->add_field( 'client_secret', __( 'OJS Client Secret', 'wp-ojs-sso-bridge' ), 'text', 'oidc' );
+ $this->add_field( 'redirect_uri', __( 'OJS Redirect URI', 'wp-ojs-sso-bridge' ), 'url', 'oidc' );
+ $this->add_field( 'claim_name', __( 'OIDC Claim Name', 'wp-ojs-sso-bridge' ), 'text', 'oidc', 'subscription_active' );
+
+ // --- User Meta Section ---
+ add_settings_section( 'meta', __( 'User Meta', 'wp-ojs-sso-bridge' ), '__return_false', 'wp-ojs-sso-bridge' );
+
+ $this->add_field( 'user_meta_key', __( 'Subscription Meta Key', 'wp-ojs-sso-bridge' ), 'text', 'meta', 'ojs_subscription_active' );
+ $this->add_field( 'user_meta_label', __( 'Subscription Label', 'wp-ojs-sso-bridge' ), 'text', 'meta', __( 'OJS Subscription Active', 'wp-ojs-sso-bridge' ) );
+
+ // --- Registration Section ---
+ add_settings_section( 'registration', __( 'Registration Form', 'wp-ojs-sso-bridge' ), '__return_false', 'wp-ojs-sso-bridge' );
+
+ $this->add_field( 'enable_registration_checkbox', __( 'Enable subscription request at registration', 'wp-ojs-sso-bridge' ), 'checkbox', 'registration' );
+ $this->add_field( 'registration_checkbox_text', __( 'Checkbox label', 'wp-ojs-sso-bridge' ), 'text', 'registration', __( 'I want to subscribe to the journal', 'wp-ojs-sso-bridge' ) );
+ $this->add_field( 'registration_description', __( 'Description below checkbox (HTML allowed)', 'wp-ojs-sso-bridge' ), 'textarea', 'registration', __( 'Your subscription will be activated after verification by an administrator.', 'wp-ojs-sso-bridge' ) );
+ $this->add_field( 'notify_admin_on_request', __( 'Notify admin on subscription request', 'wp-ojs-sso-bridge' ), 'checkbox', 'registration' );
+
+ // --- Cleanup Section ---
+ add_settings_section( 'cleanup', __( 'Uninstall', 'wp-ojs-sso-bridge' ), '__return_false', 'wp-ojs-sso-bridge' );
+
+ $this->add_field( 'cleanup_on_uninstall', __( 'Remove all user data on uninstall', 'wp-ojs-sso-bridge' ), 'checkbox', 'cleanup' );
+ }
+
+ private function add_field( $id, $title, $type, $section, $default = '' ) {
+ add_settings_field(
+ $id,
+ $title,
+ [ $this, 'render_field' ],
+ 'wp-ojs-sso-bridge',
+ $section,
+ [ 'id' => $id, 'type' => $type, 'default' => $default ]
+ );
+ }
+
+ public function render_field( $args ) {
+ $opts = WP_OJS_SSO_Plugin::get_option();
+ $id = $args['id'];
+ $type = $args['type'];
+ $value = $opts[ $id ] ?? $args['default'];
+ $name = "wp_ojs_sso_bridge_settings[{$id}]";
+
+ switch ( $type ) {
+ case 'checkbox':
+ printf(
+ ' ',
+ esc_attr( $id ),
+ esc_attr( $name ),
+ checked( $value, '1', false )
+ );
+ break;
+ case 'textarea':
+ printf(
+ '',
+ esc_attr( $id ),
+ esc_attr( $name ),
+ esc_textarea( $value )
+ );
+ if ( 'registration_description' === $id ) {
+ echo '' . esc_html__( 'You can use simple HTML (paragraphs, lists, links, bold, line breaks) for the text shown on the registration screen.', 'wp-ojs-sso-bridge' ) . '
';
+ }
+ break;
+ case 'url':
+ printf(
+ ' ',
+ esc_attr( $id ),
+ esc_attr( $name ),
+ esc_url( $value )
+ );
+ break;
+ default:
+ printf(
+ ' ',
+ esc_attr( $id ),
+ esc_attr( $name ),
+ esc_attr( $value )
+ );
+ }
+ }
+
+ public function sanitize( $input ) {
+ $clean = [];
+ $clean['client_id'] = sanitize_text_field( $input['client_id'] ?? '' );
+ $clean['client_secret'] = sanitize_text_field( $input['client_secret'] ?? '' );
+ $clean['redirect_uri'] = esc_url_raw( $input['redirect_uri'] ?? '' );
+ $clean['claim_name'] = sanitize_key( $input['claim_name'] ?? 'subscription_active' );
+ $clean['user_meta_key'] = sanitize_key( $input['user_meta_key'] ?? 'ojs_subscription_active' );
+ $clean['user_meta_label'] = sanitize_text_field( $input['user_meta_label'] ?? '' );
+ $clean['enable_registration_checkbox'] = ! empty( $input['enable_registration_checkbox'] ) ? '1' : '';
+ $clean['registration_checkbox_text'] = sanitize_text_field( $input['registration_checkbox_text'] ?? '' );
+ $clean['registration_description'] = wp_kses_post( trim( (string) ( $input['registration_description'] ?? '' ) ) );
+ $clean['notify_admin_on_request'] = ! empty( $input['notify_admin_on_request'] ) ? '1' : '';
+ $clean['cleanup_on_uninstall'] = ! empty( $input['cleanup_on_uninstall'] ) ? '1' : '';
+ return $clean;
+ }
+
+ public function render_page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+ ?>
+
+
+
+ render_generate_secret_button(); ?>
+
+
+
+ [
+ 'label' => __( 'Place of birth', 'wp-ojs-sso-bridge' ),
+ 'type' => 'text',
+ 'required' => true,
+ ],
+ 'ojs_birthdate' => [
+ 'label' => __( 'Date of birth', 'wp-ojs-sso-bridge' ),
+ 'type' => 'date',
+ 'required' => true,
+ ],
+ 'ojs_fiscal_code' => [
+ 'label' => __( 'Fiscal Code', 'wp-ojs-sso-bridge' ),
+ 'type' => 'text',
+ 'required' => true,
+ 'pattern' => '[A-Za-z0-9]{16}',
+ 'maxlength' => 16,
+ ],
+ 'ojs_shipping_address' => [
+ 'label' => __( 'Shipping address for the journal', 'wp-ojs-sso-bridge' ),
+ 'type' => 'textarea',
+ 'required' => true,
+ ],
+ 'ojs_membership_type' => [
+ 'label' => __( 'Membership type', 'wp-ojs-sso-bridge' ),
+ 'type' => 'radio',
+ 'required' => true,
+ 'options' => [
+ 'ordinary' => __( 'Ordinary member (no TugBoat subscription)', 'wp-ojs-sso-bridge' ),
+ 'ordinary_tugboat' => __( 'Ordinary member with TugBoat subscription', 'wp-ojs-sso-bridge' ),
+ 'student' => __( 'Student member (document required)', 'wp-ojs-sso-bridge' ),
+ 'student_junior' => __( 'Junior student member (document required)', 'wp-ojs-sso-bridge' ),
+ 'institutional' => __( 'Institutional member', 'wp-ojs-sso-bridge' ),
+ ],
+ ],
+ 'ojs_student_document' => [
+ 'label' => __( 'Student certificate (PDF, JPG or PNG, max 10 MB)', 'wp-ojs-sso-bridge' ),
+ 'type' => 'file',
+ 'required' => false, // conditionally required via JS + server validation
+ 'accept' => '.pdf,.jpg,.jpeg,.png',
+ 'meta_key' => 'ojs_student_document', // may store attachment ID or legacy path
+ 'conditional' => [ 'ojs_membership_type' => [ 'student', 'student_junior' ] ],
+ ],
+ 'ojs_bonifico_receipt' => [
+ 'label' => __( 'Bank transfer receipt (PDF, JPG or PNG, max 10 MB)', 'wp-ojs-sso-bridge' ),
+ 'type' => 'file',
+ 'required' => false, // required in validate() for subscription requests
+ 'accept' => '.pdf,.jpg,.jpeg,.png',
+ 'meta_key' => 'ojs_bonifico_attachment_id', // file input name ojs_bonifico_receipt, meta stores attachment id
+ ],
+ 'ojs_consent_guit_soci' => [
+ 'label' => __( 'I consent to being added to the guit-soci mailing list for institutional communications (mandatory).', 'wp-ojs-sso-bridge' ),
+ 'type' => 'checkbox',
+ 'required' => true,
+ ],
+ 'ojs_consent_guit_members' => [
+ 'label' => __( 'I consent to being added to the guit-members mailing list for group activity communications.', 'wp-ojs-sso-bridge' ),
+ 'type' => 'radio',
+ 'required' => true,
+ 'options' => [
+ '1' => __( 'I consent', 'wp-ojs-sso-bridge' ),
+ '0' => __( 'I do not consent', 'wp-ojs-sso-bridge' ),
+ ],
+ ],
+ 'ojs_consent_privacy' => [
+ 'label' => __( 'I consent to the processing of personal data provided through this form for the institutional purposes of the association.', 'wp-ojs-sso-bridge' ),
+ 'type' => 'checkbox',
+ 'required' => true,
+ ],
+ ];
+ }
+
+ public static function render_field( $key, $field, $value = '' ) {
+ $required_attr = '';
+ $data_attr = '';
+ if ( ! empty( $field['required'] ) ) {
+ $required_attr = ' data-conditional-required="1"';
+ }
+ if ( ! empty( $field['conditional'] ) ) {
+ $data_attr = sprintf( ' data-show-when="%s"', esc_attr( wp_json_encode( $field['conditional'] ) ) );
+ }
+
+ $wrapper_id = 'ojs-field-' . esc_attr( $key );
+ $is_conditional = ! empty( $field['conditional'] );
+ $style = $is_conditional ? ' style="display:none;"' : '';
+
+ echo '';
+ echo '
';
+ echo esc_html( $field['label'] );
+ if ( ! empty( $field['required'] ) ) {
+ echo ' * ';
+ }
+ echo ' ';
+
+ switch ( $field['type'] ) {
+ case 'text':
+ printf(
+ '
',
+ esc_attr( $key ),
+ esc_attr( $value ),
+ $required_attr,
+ ! empty( $field['pattern'] ) ? 'pattern="' . esc_attr( $field['pattern'] ) . '"' : '',
+ ! empty( $field['maxlength'] ) ? 'maxlength="' . (int) $field['maxlength'] . '"' : ''
+ );
+ break;
+
+ case 'date':
+ printf(
+ '
',
+ esc_attr( $key ),
+ esc_attr( $value ),
+ $required_attr
+ );
+ break;
+
+ case 'textarea':
+ printf(
+ '
',
+ esc_attr( $key ),
+ $required_attr,
+ esc_textarea( $value )
+ );
+ break;
+
+ case 'radio':
+ foreach ( $field['options'] as $opt_val => $opt_label ) {
+ printf(
+ '
%4$s',
+ esc_attr( $key ),
+ esc_attr( $opt_val ),
+ checked( $value, (string) $opt_val, false ),
+ esc_html( $opt_label )
+ );
+ }
+ break;
+
+ case 'checkbox':
+ printf(
+ '
%4$s',
+ esc_attr( $key ),
+ checked( $value, '1', false ),
+ $required_attr,
+ '' // label is already above
+ );
+ break;
+
+ case 'file':
+ printf(
+ '
',
+ esc_attr( $key ),
+ esc_attr( $field['accept'] ?? '' )
+ );
+ if ( $value ) {
+ $label = $value;
+ if ( is_numeric( $value ) && (int) $value > 0 ) {
+ $p = get_post( (int) $value );
+ if ( $p && 'attachment' === $p->post_type ) {
+ $f = get_attached_file( (int) $value );
+ $label = $f ? wp_basename( $f ) : $p->post_title;
+ }
+ } elseif ( is_string( $value ) && ( false !== strpos( $value, '/' ) || false !== strpos( $value, '\\' ) ) ) {
+ $label = wp_basename( $value );
+ }
+ echo '
' . esc_html__( 'Current file:', 'wp-ojs-sso-bridge' ) . ' ' . esc_html( $label ) . '
';
+ }
+ break;
+ }
+
+ echo '
';
+ }
+
+ public static function validate( $data, $files = [], $for_user_id = null ) {
+ $errors = [];
+ $fields = self::get_fields();
+ $uid = ( null !== $for_user_id ) ? (int) $for_user_id : get_current_user_id();
+
+ $required_text = [ 'ojs_birthplace', 'ojs_birthdate', 'ojs_fiscal_code', 'ojs_shipping_address', 'ojs_membership_type' ];
+ foreach ( $required_text as $key ) {
+ if ( empty( $data[ $key ] ) ) {
+ $errors[ $key ] = sprintf(
+ /* translators: %s: field label */
+ __( 'The field "%s" is required for subscription.', 'wp-ojs-sso-bridge' ),
+ $fields[ $key ]['label']
+ );
+ }
+ }
+
+ if ( ! empty( $data['ojs_fiscal_code'] ) && ! preg_match( '/^[A-Z0-9]{16}$/i', $data['ojs_fiscal_code'] ) ) {
+ $errors['ojs_fiscal_code'] = __( 'The Fiscal Code must be exactly 16 alphanumeric characters.', 'wp-ojs-sso-bridge' );
+ }
+
+ $type = sanitize_text_field( $data['ojs_membership_type'] ?? '' );
+ if ( in_array( $type, [ 'student', 'student_junior' ], true ) ) {
+ if ( empty( $files['ojs_student_document']['name'] ) ) {
+ $has = class_exists( 'WP_OJS_SSO_Subscription_Media' ) && $uid > 0
+ ? WP_OJS_SSO_Subscription_Media::has_stored_file( $uid, WP_OJS_SSO_Subscription_Media::KIND_CERTIFICATO )
+ : false;
+ if ( ! $has ) {
+ $errors['ojs_student_document'] = __( 'A student certificate document is required for student memberships.', 'wp-ojs-sso-bridge' );
+ }
+ }
+ }
+
+ if ( empty( $files['ojs_bonifico_receipt']['name'] ?? '' ) ) {
+ $has_b = class_exists( 'WP_OJS_SSO_Subscription_Media' ) && $uid > 0
+ ? WP_OJS_SSO_Subscription_Media::has_stored_file( $uid, WP_OJS_SSO_Subscription_Media::KIND_BONIFICO )
+ : false;
+ if ( ! $has_b ) {
+ $errors['ojs_bonifico_receipt'] = __( 'A bank transfer receipt is required for subscription requests.', 'wp-ojs-sso-bridge' );
+ }
+ }
+
+ if ( empty( $data['ojs_consent_guit_soci'] ) ) {
+ $errors['ojs_consent_guit_soci'] = __( 'Consent to the guit-soci mailing list is mandatory.', 'wp-ojs-sso-bridge' );
+ }
+
+ if ( ! isset( $data['ojs_consent_guit_members'] ) || '' === $data['ojs_consent_guit_members'] ) {
+ $errors['ojs_consent_guit_members'] = __( 'Please indicate your preference for the guit-members mailing list.', 'wp-ojs-sso-bridge' );
+ }
+
+ if ( empty( $data['ojs_consent_privacy'] ) ) {
+ $errors['ojs_consent_privacy'] = __( 'Consent to data processing is mandatory.', 'wp-ojs-sso-bridge' );
+ }
+
+ return $errors;
+ }
+
+ public static function save_fields( $user_id, $data ) {
+ $text_fields = [
+ 'ojs_birthplace', 'ojs_birthdate', 'ojs_fiscal_code',
+ 'ojs_shipping_address', 'ojs_membership_type',
+ 'ojs_consent_guit_soci', 'ojs_consent_guit_members', 'ojs_consent_privacy',
+ ];
+
+ foreach ( $text_fields as $key ) {
+ if ( isset( $data[ $key ] ) ) {
+ update_user_meta( $user_id, $key, sanitize_text_field( $data[ $key ] ) );
+ }
+ }
+
+ if ( isset( $data['first_name'] ) ) {
+ update_user_meta( $user_id, 'first_name', sanitize_text_field( $data['first_name'] ) );
+ }
+ if ( isset( $data['last_name'] ) ) {
+ update_user_meta( $user_id, 'last_name', sanitize_text_field( $data['last_name'] ) );
+ }
+ }
+}
diff --git a/includes/class-user-meta.php b/includes/class-user-meta.php
new file mode 100644
index 0000000..26d80bd
--- /dev/null
+++ b/includes/class-user-meta.php
@@ -0,0 +1,212 @@
+meta_label();
+ $columns['ojs_sub_files'] = __( 'Subscription documents', 'wp-ojs-sso-bridge' );
+ return $columns;
+ }
+
+ public function column_content( $value, $column_name, $user_id ) {
+ if ( 'ojs_sub_files' === $column_name ) {
+ return $this->documents_column_html( (int) $user_id );
+ }
+
+ if ( 'ojs_subscription' !== $column_name ) {
+ return $value;
+ }
+
+ $active = (bool) get_user_meta( $user_id, $this->meta_key(), true );
+ $requested = (bool) get_user_meta( $user_id, 'ojs_subscription_requested', true );
+
+ if ( $active ) {
+ return '' . esc_html__( 'Active', 'wp-ojs-sso-bridge' ) . ' ';
+ }
+ if ( $requested ) {
+ return '' . esc_html__( 'Requested', 'wp-ojs-sso-bridge' ) . ' ';
+ }
+ return '—';
+ }
+
+ /**
+ * Bonifico + Certificato links (or em dash) for the users list table.
+ *
+ * @param int $user_id User ID.
+ * @return string
+ */
+ private function documents_column_html( $user_id ) {
+ if ( ! class_exists( 'WP_OJS_SSO_Subscription_Media' ) ) {
+ return '—';
+ }
+
+ $dash = '—';
+
+ $bid = WP_OJS_SSO_Subscription_Media::get_attachment_id( $user_id, WP_OJS_SSO_Subscription_Media::KIND_BONIFICO );
+ if ( $bid > 0 && current_user_can( 'edit_post', $bid ) ) {
+ $blink = get_edit_post_link( $bid, 'raw' );
+ $bon = $blink
+ ? '' . esc_html__( 'Open', 'wp-ojs-sso-bridge' ) . ' '
+ : $dash;
+ } else {
+ $bon = $dash;
+ }
+
+ $cid = WP_OJS_SSO_Subscription_Media::get_attachment_id( $user_id, WP_OJS_SSO_Subscription_Media::KIND_CERTIFICATO );
+ if ( $cid > 0 && current_user_can( 'edit_post', $cid ) ) {
+ $clink = get_edit_post_link( $cid, 'raw' );
+ $cert = $clink
+ ? '' . esc_html__( 'Open', 'wp-ojs-sso-bridge' ) . ' '
+ : $dash;
+ } else {
+ $legacy = WP_OJS_SSO_Subscription_Media::get_legacy_path( $user_id );
+ if ( $legacy && is_readable( $legacy ) ) {
+ $durl = WP_OJS_SSO_File_Upload::get_admin_download_url( $user_id );
+ $cert = '' . esc_html__( 'Download', 'wp-ojs-sso-bridge' ) . ' ';
+ } else {
+ $cert = $dash;
+ }
+ }
+
+ return sprintf(
+ /* translators: 1: Bonifico label, 2: link or dash, 3: Certificato label, 4: link or dash */
+ '%1$s %2$s · %3$s %4$s ',
+ '' . esc_html__( 'Bank transfer receipt', 'wp-ojs-sso-bridge' ) . ': ' . esc_html__( 'Bonifico', 'wp-ojs-sso-bridge' ) . ':',
+ $bon,
+ esc_html__( 'Certificato', 'wp-ojs-sso-bridge' ) . ':',
+ $cert
+ );
+ }
+
+ public function sortable_column( $columns ) {
+ $columns['ojs_subscription'] = 'ojs_subscription';
+ return $columns;
+ }
+
+ public function sort_by_meta( $query ) {
+ if ( ! is_admin() || 'ojs_subscription' !== ( $query->get( 'orderby' ) ) ) {
+ return;
+ }
+ $query->set( 'meta_key', $this->meta_key() );
+ $query->set( 'orderby', 'meta_value' );
+ }
+
+ public function register_bulk_actions( $actions ) {
+ $actions['ojs_activate_subscription'] = __( 'Activate OJS Subscription', 'wp-ojs-sso-bridge' );
+ $actions['ojs_deactivate_subscription'] = __( 'Deactivate OJS Subscription', 'wp-ojs-sso-bridge' );
+ return $actions;
+ }
+
+ public function handle_bulk_actions( $redirect_to, $doaction, $user_ids ) {
+ if ( ! current_user_can( 'edit_users' ) ) {
+ return $redirect_to;
+ }
+
+ $key = $this->meta_key();
+
+ if ( 'ojs_activate_subscription' === $doaction ) {
+ foreach ( $user_ids as $uid ) {
+ update_user_meta( $uid, $key, '1' );
+ do_action( 'wp_ojs_sso_subscription_changed', $uid, true );
+ }
+ $redirect_to = add_query_arg( 'ojs_bulk_activated', count( $user_ids ), $redirect_to );
+ }
+
+ if ( 'ojs_deactivate_subscription' === $doaction ) {
+ foreach ( $user_ids as $uid ) {
+ $old = (bool) get_user_meta( $uid, $key, true );
+ update_user_meta( $uid, $key, '' );
+ if ( $old ) {
+ $sessions = WP_Session_Tokens::get_instance( $uid );
+ $sessions->destroy_all();
+ }
+ do_action( 'wp_ojs_sso_subscription_changed', $uid, false );
+ }
+ $redirect_to = add_query_arg( 'ojs_bulk_deactivated', count( $user_ids ), $redirect_to );
+ }
+
+ return $redirect_to;
+ }
+
+ public function filter_dropdown( $which ) {
+ if ( 'top' !== $which ) {
+ return;
+ }
+ $current = $_GET['ojs_sub_filter'] ?? '';
+ ?>
+
+
+ >
+ >
+ >
+
+ meta_key();
+ $meta_query = $query->get( 'meta_query' ) ?: [];
+
+ switch ( $filter ) {
+ case 'active':
+ $meta_query[] = [ 'key' => $key, 'value' => '1', 'compare' => '=' ];
+ break;
+ case 'requested':
+ $meta_query[] = [ 'key' => 'ojs_subscription_requested', 'value' => '1', 'compare' => '=' ];
+ $meta_query[] = [
+ 'relation' => 'OR',
+ [ 'key' => $key, 'compare' => 'NOT EXISTS' ],
+ [ 'key' => $key, 'value' => '1', 'compare' => '!=' ],
+ ];
+ break;
+ case 'none':
+ $meta_query[] = [
+ 'relation' => 'OR',
+ [ 'key' => 'ojs_subscription_requested', 'compare' => 'NOT EXISTS' ],
+ [ 'key' => 'ojs_subscription_requested', 'value' => '1', 'compare' => '!=' ],
+ ];
+ break;
+ }
+
+ $query->set( 'meta_query', $meta_query );
+ }
+}
diff --git a/includes/class-user-subscription-media.php b/includes/class-user-subscription-media.php
new file mode 100644
index 0000000..cda7f12
--- /dev/null
+++ b/includes/class-user-subscription-media.php
@@ -0,0 +1,285 @@
+user_nicename );
+ if ( $slug === '' ) {
+ $slug = 'user-' . (int) $user_id;
+ }
+ return $slug;
+ }
+
+ /**
+ * Relative subdir under basedir, no leading/trailing slash issues (leading slash for subdir in WP is ok).
+ */
+ public static function get_subdir( $user_id, $kind ) {
+ $slug = self::get_user_folder_slug( $user_id );
+ return self::BASE_DIR . '/' . $slug . '/' . $kind;
+ }
+
+ /**
+ * @param string $kind self::KIND_*
+ */
+ public static function handle_upload( $user_id, $file_key, $kind ) {
+ $user_id = (int) $user_id;
+ if ( $user_id < 1 || ( self::KIND_BONIFICO !== $kind && self::KIND_CERTIFICATO !== $kind ) ) {
+ return new WP_Error( 'invalid_args', __( 'Invalid upload request.', 'wp-ojs-sso-bridge' ) );
+ }
+
+ if ( empty( $_FILES[ $file_key ]['name'] ) ) {
+ return null;
+ }
+
+ $file = $_FILES[ $file_key ];
+ if ( $file['error'] !== UPLOAD_ERR_OK ) {
+ return new WP_Error( 'upload_error', __( 'File upload failed.', 'wp-ojs-sso-bridge' ) );
+ }
+ if ( $file['size'] > self::$max_size ) {
+ return new WP_Error( 'file_too_large', __( 'The file exceeds the maximum size of 10 MB.', 'wp-ojs-sso-bridge' ) );
+ }
+ $finfo = finfo_open( FILEINFO_MIME_TYPE );
+ $mimetype = finfo_file( $finfo, $file['tmp_name'] );
+ finfo_close( $finfo );
+ if ( ! in_array( $mimetype, self::$allowed_mimes, true ) ) {
+ return new WP_Error( 'invalid_type', __( 'Only PDF, JPG and PNG files are allowed.', 'wp-ojs-sso-bridge' ) );
+ }
+
+ if ( ! function_exists( 'media_handle_upload' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+ }
+
+ $old_attachment_id = self::get_attachment_id( $user_id, $kind );
+ $old_legacy_path = ( self::KIND_CERTIFICATO === $kind ) ? self::get_legacy_path( $user_id ) : '';
+
+ $upload_dir = wp_upload_dir();
+ $subdir = self::get_subdir( $user_id, $kind );
+ $abs_target_dir = $upload_dir['basedir'] . '/' . $subdir;
+ if ( ! is_dir( $abs_target_dir ) ) {
+ wp_mkdir_p( $abs_target_dir );
+ }
+
+ self::$filter_subdir = $subdir;
+ add_filter( 'upload_dir', [ __CLASS__, 'filter_upload_dir' ] );
+
+ self::$bypass_upload_cap = true;
+ add_filter( 'user_has_cap', [ __CLASS__, 'filter_user_has_cap' ], 10, 4 );
+
+ $prev = get_current_user_id();
+ wp_set_current_user( $user_id );
+
+ // Use default $overrides (test_form => false) so registration without admin POST still works.
+ $attach_id = media_handle_upload( $file_key, 0 );
+
+ wp_set_current_user( $prev );
+
+ remove_filter( 'user_has_cap', [ __CLASS__, 'filter_user_has_cap' ], 10 );
+ self::$bypass_upload_cap = false;
+
+ remove_filter( 'upload_dir', [ __CLASS__, 'filter_upload_dir' ] );
+ self::$filter_subdir = null;
+
+ if ( is_wp_error( $attach_id ) ) {
+ if ( is_array( $file ) && ! empty( $file['name'] ) ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( '[wp-ojs-sso-bridge] media upload failed: ' . $attach_id->get_error_message() );
+ }
+ return $attach_id;
+ }
+
+ wp_update_post(
+ [
+ 'ID' => (int) $attach_id,
+ 'post_author' => $user_id,
+ ]
+ );
+
+ if ( $old_attachment_id && (int) $old_attachment_id !== (int) $attach_id ) {
+ wp_delete_attachment( (int) $old_attachment_id, true );
+ } elseif ( $old_legacy_path && file_exists( $old_legacy_path ) ) {
+ @unlink( $old_legacy_path );
+ }
+
+ if ( self::KIND_BONIFICO === $kind ) {
+ update_user_meta( $user_id, 'ojs_bonifico_attachment_id', (string) (int) $attach_id );
+ } else {
+ update_user_meta( $user_id, 'ojs_student_document', (string) (int) $attach_id );
+ }
+
+ return (int) $attach_id;
+ }
+
+ public static function filter_upload_dir( $dirs ) {
+ if ( ! self::$filter_subdir ) {
+ return $dirs;
+ }
+ $subdir = self::$filter_subdir;
+ $dirs['subdir'] = '/' . ltrim( $subdir, '/' );
+ $dirs['path'] = $dirs['basedir'] . $dirs['subdir'];
+ $dirs['url'] = $dirs['baseurl'] . $dirs['subdir'];
+ return $dirs;
+ }
+
+ public static function filter_user_has_cap( $allcaps, $caps, $args, $user ) {
+ if ( ! self::$bypass_upload_cap || empty( $caps ) ) {
+ return $allcaps;
+ }
+ if ( in_array( 'upload_files', (array) $caps, true ) || ( isset( $args[0] ) && 'upload_files' === $args[0] ) ) {
+ $allcaps['upload_files'] = true;
+ }
+ return $allcaps;
+ }
+
+ /**
+ * @return int|string path for legacy, int attachment id, or empty
+ */
+ public static function get_student_meta_value( $user_id ) {
+ return get_user_meta( (int) $user_id, 'ojs_student_document', true );
+ }
+
+ /**
+ * @return int|string
+ */
+ public static function get_legacy_path( $user_id ) {
+ $v = self::get_student_meta_value( $user_id );
+ if ( $v && is_string( $v ) && false !== strpos( $v, 'ojs-sso-bridge' ) ) {
+ return $v;
+ }
+ if ( $v && is_string( $v ) && ! is_numeric( $v ) && ( ( false !== strpos( $v, '/' ) && file_exists( $v ) ) || ( false !== strpos( $v, '\\' ) && file_exists( $v ) ) ) ) {
+ return $v;
+ }
+ return '';
+ }
+
+ /**
+ * @param string $kind KIND_BONIFICO|KIND_CERTIFICATO
+ */
+ public static function get_attachment_id( $user_id, $kind ) {
+ $user_id = (int) $user_id;
+ if ( self::KIND_BONIFICO === $kind ) {
+ $v = get_user_meta( $user_id, 'ojs_bonifico_attachment_id', true );
+ return ( $v && (int) $v > 0 ) ? (int) $v : 0;
+ }
+ $v = self::get_student_meta_value( $user_id );
+ if ( $v && is_numeric( $v ) && (int) $v > 0 && 'attachment' === get_post_type( (int) $v ) ) {
+ return (int) $v;
+ }
+ return 0;
+ }
+
+ /**
+ * @param string $kind KIND_*
+ */
+ public static function has_stored_file( $user_id, $kind ) {
+ if ( self::KIND_BONIFICO === $kind ) {
+ $id = self::get_attachment_id( $user_id, $kind );
+ if ( $id > 0 && get_post( $id ) ) {
+ return true;
+ }
+ return false;
+ }
+ if ( self::get_attachment_id( $user_id, $kind ) > 0 ) {
+ return true;
+ }
+ $legacy = self::get_legacy_path( $user_id );
+ return $legacy && is_readable( $legacy );
+ }
+
+ /**
+ * @param string $kind KIND_*
+ */
+ public static function get_edit_link( $user_id, $kind ) {
+ $id = self::get_attachment_id( $user_id, $kind );
+ if ( $id < 1 ) {
+ return '';
+ }
+ if ( current_user_can( 'edit_post', $id ) ) {
+ $link = get_edit_post_link( $id, 'raw' );
+ return $link ? $link : '';
+ }
+ $url = wp_get_attachment_url( $id );
+ return $url ? $url : '';
+ }
+
+ /**
+ * @param string $kind KIND_*
+ */
+ public static function get_file_label( $user_id, $kind ) {
+ $id = self::get_attachment_id( $user_id, $kind );
+ if ( $id > 0 ) {
+ $p = get_post( $id );
+ return $p ? $p->post_title : (string) $id;
+ }
+ if ( self::KIND_CERTIFICATO === $kind ) {
+ $legacy = self::get_legacy_path( $user_id );
+ if ( $legacy ) {
+ return basename( $legacy );
+ }
+ }
+ return '';
+ }
+
+ /**
+ * @param int[] $user_ids
+ */
+ public static function on_delete_user( $user_id ) {
+ $user_id = (int) $user_id;
+ $b = self::get_attachment_id( $user_id, self::KIND_BONIFICO );
+ if ( $b > 0 ) {
+ wp_delete_attachment( $b, true );
+ }
+ $s = self::get_attachment_id( $user_id, self::KIND_CERTIFICATO );
+ if ( $s > 0 ) {
+ wp_delete_attachment( $s, true );
+ }
+ $legacy = self::get_legacy_path( $user_id );
+ if ( $legacy && is_file( $legacy ) ) {
+ @unlink( $legacy );
+ }
+ $old_dir = WP_OJS_SSO_File_Upload::get_upload_dir( $user_id );
+ if ( is_dir( $old_dir ) ) {
+ $files = glob( $old_dir . '/*' );
+ if ( $files ) {
+ array_map( 'unlink', array_filter( $files, 'is_file' ) );
+ }
+ @rmdir( $old_dir );
+ }
+ }
+}
diff --git a/languages/wp-ojs-sso-bridge-it_IT.mo b/languages/wp-ojs-sso-bridge-it_IT.mo
new file mode 100644
index 0000000..fbbc5c8
--- /dev/null
+++ b/languages/wp-ojs-sso-bridge-it_IT.mo
Binary files differ
diff --git a/languages/wp-ojs-sso-bridge-it_IT.po b/languages/wp-ojs-sso-bridge-it_IT.po
new file mode 100644
index 0000000..ff8e7b7
--- /dev/null
+++ b/languages/wp-ojs-sso-bridge-it_IT.po
@@ -0,0 +1,277 @@
+# Italian translations for WP OJS SSO Bridge
+# Copyright (C) 2026 GuIT
+# This file is distributed under the GPL-2.0-or-later.
+msgid ""
+msgstr ""
+"Project-Id-Version: WP OJS SSO Bridge 1.1.0\n"
+"PO-Revision-Date: 2026-04-16T00:00:00+00:00\n"
+"Last-Translator: GuIT \n"
+"Language-Team: Italian\n"
+"Language: it_IT\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "WP OJS SSO Bridge requires the \"OpenID Connect Server\" plugin by Automattic for SSO functionality."
+msgstr "WP OJS SSO Bridge richiede il plugin \"OpenID Connect Server\" di Automattic per la funzionalita' SSO."
+
+msgid "OJS SSO Bridge"
+msgstr "OJS SSO Bridge"
+
+msgid "Settings"
+msgstr "Impostazioni"
+
+msgid "OpenID Connect"
+msgstr "OpenID Connect"
+
+msgid "OJS Client ID"
+msgstr "Client ID OJS"
+
+msgid "OJS Client Secret"
+msgstr "Client Secret OJS"
+
+msgid "OJS Redirect URI"
+msgstr "URI di Redirect OJS"
+
+msgid "OIDC Claim Name"
+msgstr "Nome claim OIDC"
+
+msgid "User Meta"
+msgstr "Meta utente"
+
+msgid "Subscription Meta Key"
+msgstr "Chiave meta sottoscrizione"
+
+msgid "Subscription Label"
+msgstr "Etichetta sottoscrizione"
+
+msgid "OJS Subscription Active"
+msgstr "Sottoscrizione OJS attiva"
+
+msgid "Registration Form"
+msgstr "Form di registrazione"
+
+msgid "Enable subscription request at registration"
+msgstr "Abilita richiesta sottoscrizione alla registrazione"
+
+msgid "Checkbox label"
+msgstr "Testo checkbox"
+
+msgid "I want to subscribe to the journal"
+msgstr "Desidero sottoscrivere la rivista"
+
+msgid "Description below checkbox (HTML allowed)"
+msgstr "Descrizione sotto il checkbox (HTML consentito)"
+
+msgid "You can use simple HTML (paragraphs, lists, links, bold, line breaks) for the text shown on the registration screen."
+msgstr "Puoi usare HTML semplice (paragrafi, elenchi, link, grassetto, a capo) per il testo mostrato nella schermata di registrazione."
+
+msgid "Your subscription will be activated after verification by an administrator."
+msgstr "La sottoscrizione verra' attivata dopo la verifica da parte di un amministratore."
+
+msgid "Notify admin on subscription request"
+msgstr "Notifica l'amministratore alla richiesta di sottoscrizione"
+
+msgid "Uninstall"
+msgstr "Disinstallazione"
+
+msgid "Remove all user data on uninstall"
+msgstr "Rimuovi tutti i dati utente alla disinstallazione"
+
+msgid "Generate Secret"
+msgstr "Genera Secret"
+
+msgid "OJS Subscription"
+msgstr "Sottoscrizione OJS"
+
+msgid "Active"
+msgstr "Attiva"
+
+msgid "Requested"
+msgstr "Richiesta"
+
+msgid "Activate OJS Subscription"
+msgstr "Attiva sottoscrizione OJS"
+
+msgid "Deactivate OJS Subscription"
+msgstr "Disattiva sottoscrizione OJS"
+
+msgid "All subscriptions"
+msgstr "Tutte le sottoscrizioni"
+
+msgid "Requested (pending)"
+msgstr "Richieste in attesa"
+
+msgid "Not requested"
+msgstr "Non richieste"
+
+msgid "Place of birth"
+msgstr "Luogo di nascita"
+
+msgid "Date of birth"
+msgstr "Data di nascita"
+
+msgid "Fiscal Code"
+msgstr "Codice Fiscale"
+
+msgid "Shipping address for the journal"
+msgstr "Indirizzo per la spedizione della rivista"
+
+msgid "Membership type"
+msgstr "Tipo di iscrizione"
+
+msgid "Ordinary member (no TugBoat subscription)"
+msgstr "Socio ordinario (senza abbonamento TugBoat)"
+
+msgid "Ordinary member with TugBoat subscription"
+msgstr "Socio ordinario con abbonamento TugBoat"
+
+msgid "Student member (document required)"
+msgstr "Socio studente (documento richiesto)"
+
+msgid "Junior student member (document required)"
+msgstr "Socio studente junior (documento richiesto)"
+
+msgid "Institutional member"
+msgstr "Socio istituzionale"
+
+msgid "Student certificate (PDF, JPG or PNG, max 10 MB)"
+msgstr "Documento che attesti la qualita' di studente (PDF, JPG o PNG, max 10 MB)"
+
+msgid "I consent to being added to the guit-soci mailing list for institutional communications (mandatory)."
+msgstr "Acconsento all'iscrizione alla mailing list guit-soci per comunicazioni istituzionali (obbligatorio)."
+
+msgid "I consent to being added to the guit-members mailing list for group activity communications."
+msgstr "Acconsento all'iscrizione alla mailing list guit-members per comunicazioni sull'attivita' del gruppo."
+
+msgid "I consent"
+msgstr "Acconsento"
+
+msgid "I do not consent"
+msgstr "Non acconsento"
+
+msgid "I consent to the processing of personal data provided through this form for the institutional purposes of the association."
+msgstr "Acconsento al trattamento dei dati personali forniti tramite questo modulo per le finalita' istituzionali dell'associazione."
+
+msgid "Current file:"
+msgstr "File attuale:"
+
+msgid "The field \"%s\" is required for subscription."
+msgstr "Il campo \"%s\" e' obbligatorio per la sottoscrizione."
+
+msgid "The Fiscal Code must be exactly 16 alphanumeric characters."
+msgstr "Il Codice Fiscale deve essere di 16 caratteri alfanumerici."
+
+msgid "A student certificate document is required for student memberships."
+msgstr "Il documento che attesti la qualita' di studente e' obbligatorio per le iscrizioni studente."
+
+msgid "Consent to the guit-soci mailing list is mandatory."
+msgstr "Il consenso alla mailing list guit-soci e' obbligatorio."
+
+msgid "Please indicate your preference for the guit-members mailing list."
+msgstr "Indica la tua preferenza per la mailing list guit-members."
+
+msgid "Consent to data processing is mandatory."
+msgstr "Il consenso al trattamento dei dati e' obbligatorio."
+
+msgid "File upload failed."
+msgstr "Caricamento file fallito."
+
+msgid "The file exceeds the maximum size of 10 MB."
+msgstr "Il file supera la dimensione massima di 10 MB."
+
+msgid "Only PDF, JPG and PNG files are allowed."
+msgstr "Sono accettati solo file PDF, JPG e PNG."
+
+msgid "Could not save the uploaded file."
+msgstr "Impossibile salvare il file caricato."
+
+msgid "Unauthorized"
+msgstr "Non autorizzato"
+
+msgid "Invalid nonce"
+msgstr "Nonce non valido"
+
+msgid "File not found"
+msgstr "File non trovato"
+
+msgid "First Name"
+msgstr "Nome"
+
+msgid "Last Name"
+msgstr "Cognome"
+
+msgid "Journal Subscription"
+msgstr "Sottoscrizione rivista"
+
+msgid "Status"
+msgstr "Stato"
+
+msgid "Requested – pending approval"
+msgstr "Richiesta – in attesa di approvazione"
+
+msgid "Not subscribed"
+msgstr "Non iscritto"
+
+msgid "Download"
+msgstr "Scarica"
+
+msgid "Upload a new file to replace the existing one."
+msgstr "Carica un nuovo file per sostituire quello esistente."
+
+msgid "Deactivating the subscription will immediately disconnect this user from WordPress and revoke their access to the journal on OJS. Continue?"
+msgstr "Disattivando la sottoscrizione l'utente verra' disconnesso immediatamente da WordPress e perdera' l'accesso alla rivista su OJS. Continuare?"
+
+msgid "[%s] New journal subscription request"
+msgstr "[%s] Nuova richiesta di sottoscrizione alla rivista"
+
+msgid "User \"%1$s\" (%2$s) has requested a journal subscription.\n\nReview pending requests: %3$s"
+msgstr "L'utente \"%1$s\" (%2$s) ha richiesto una sottoscrizione alla rivista.\n\nRevisiona le richieste in attesa: %3$s"
+
+msgid "User \"%1$s\" (%2$s) has requested a journal subscription from their profile.\n\nReview: %3$s"
+msgstr "L'utente \"%1$s\" (%2$s) ha richiesto una sottoscrizione dalla pagina profilo.\n\nRevisiona: %3$s"
+
+msgid "(user has requested subscription)"
+msgstr "(l'utente ha richiesto la sottoscrizione)"
+
+msgid "Error"
+msgstr "Errore"
+
+msgid "Error : First Name is required for subscription."
+msgstr "Errore : il Nome e' obbligatorio per la sottoscrizione."
+
+msgid "Error : Last Name is required for subscription."
+msgstr "Errore : il Cognome e' obbligatorio per la sottoscrizione."
+
+msgid "Bank transfer receipt (PDF, JPG or PNG, max 10 MB)"
+msgstr "Ricevuta del bonifico (PDF, JPG o PNG, max 10 MB)"
+
+msgid "A bank transfer receipt is required for subscription requests."
+msgstr "La ricevuta del bonifico e' obbligatoria per le richieste di sottoscrizione."
+
+msgid "Storage handler is not available."
+msgstr "Il gestore di archiviazione non e' disponibile."
+
+msgid "Invalid upload request."
+msgstr "Richiesta di caricamento non valida."
+
+msgid "Subscription documents"
+msgstr "Documenti sottoscrizione"
+
+msgid "Open"
+msgstr "Apri"
+
+msgid "Bank transfer receipt"
+msgstr "Ricevuta bonifico"
+
+msgid "Bonifico"
+msgstr "Bonifico"
+
+msgid "Certificato"
+msgstr "Certificato"
+
+msgid "Open in Media"
+msgstr "Apri in Medie"
+
+msgid "View file"
+msgstr "Visualizza file"
diff --git a/languages/wp-ojs-sso-bridge.pot b/languages/wp-ojs-sso-bridge.pot
new file mode 100644
index 0000000..b6b454e
--- /dev/null
+++ b/languages/wp-ojs-sso-bridge.pot
@@ -0,0 +1,277 @@
+# Copyright (C) 2026 GuIT - Gruppo Utilizzatori Italiani di TeX
+# This file is distributed under the GPL-2.0-or-later.
+msgid ""
+msgstr ""
+"Project-Id-Version: WP OJS SSO Bridge 1.0.0\n"
+"Report-Msgid-Bugs-To: https://github.com/nicola-arrigoni/wp-ojs-sso-bridge\n"
+"POT-Creation-Date: 2026-04-16T00:00:00+00:00\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: includes/class-plugin.php
+msgid "WP OJS SSO Bridge requires the \"OpenID Connect Server\" plugin by Automattic for SSO functionality."
+msgstr ""
+
+#: includes/class-settings.php
+msgid "OJS SSO Bridge"
+msgstr ""
+
+msgid "Settings"
+msgstr ""
+
+msgid "OpenID Connect"
+msgstr ""
+
+msgid "OJS Client ID"
+msgstr ""
+
+msgid "OJS Client Secret"
+msgstr ""
+
+msgid "OJS Redirect URI"
+msgstr ""
+
+msgid "OIDC Claim Name"
+msgstr ""
+
+msgid "User Meta"
+msgstr ""
+
+msgid "Subscription Meta Key"
+msgstr ""
+
+msgid "Subscription Label"
+msgstr ""
+
+msgid "OJS Subscription Active"
+msgstr ""
+
+msgid "Registration Form"
+msgstr ""
+
+msgid "Enable subscription request at registration"
+msgstr ""
+
+msgid "Checkbox label"
+msgstr ""
+
+msgid "I want to subscribe to the journal"
+msgstr ""
+
+msgid "Description below checkbox (HTML allowed)"
+msgstr ""
+
+msgid "You can use simple HTML (paragraphs, lists, links, bold, line breaks) for the text shown on the registration screen."
+msgstr ""
+
+msgid "Your subscription will be activated after verification by an administrator."
+msgstr ""
+
+msgid "Notify admin on subscription request"
+msgstr ""
+
+msgid "Uninstall"
+msgstr ""
+
+msgid "Remove all user data on uninstall"
+msgstr ""
+
+msgid "Generate Secret"
+msgstr ""
+
+#: includes/class-user-meta.php
+msgid "OJS Subscription"
+msgstr ""
+
+msgid "Active"
+msgstr ""
+
+msgid "Requested"
+msgstr ""
+
+msgid "Activate OJS Subscription"
+msgstr ""
+
+msgid "Deactivate OJS Subscription"
+msgstr ""
+
+msgid "All subscriptions"
+msgstr ""
+
+msgid "Requested (pending)"
+msgstr ""
+
+msgid "Not requested"
+msgstr ""
+
+#: includes/class-subscription-fields.php
+msgid "Place of birth"
+msgstr ""
+
+msgid "Date of birth"
+msgstr ""
+
+msgid "Fiscal Code"
+msgstr ""
+
+msgid "Shipping address for the journal"
+msgstr ""
+
+msgid "Membership type"
+msgstr ""
+
+msgid "Ordinary member (no TugBoat subscription)"
+msgstr ""
+
+msgid "Ordinary member with TugBoat subscription"
+msgstr ""
+
+msgid "Student member (document required)"
+msgstr ""
+
+msgid "Junior student member (document required)"
+msgstr ""
+
+msgid "Institutional member"
+msgstr ""
+
+msgid "Student certificate (PDF, JPG or PNG, max 10 MB)"
+msgstr ""
+
+msgid "I consent to being added to the guit-soci mailing list for institutional communications (mandatory)."
+msgstr ""
+
+msgid "I consent to being added to the guit-members mailing list for group activity communications."
+msgstr ""
+
+msgid "I consent"
+msgstr ""
+
+msgid "I do not consent"
+msgstr ""
+
+msgid "I consent to the processing of personal data provided through this form for the institutional purposes of the association."
+msgstr ""
+
+msgid "Current file:"
+msgstr ""
+
+msgid "The field \"%s\" is required for subscription."
+msgstr ""
+
+msgid "The Fiscal Code must be exactly 16 alphanumeric characters."
+msgstr ""
+
+msgid "A student certificate document is required for student memberships."
+msgstr ""
+
+msgid "Consent to the guit-soci mailing list is mandatory."
+msgstr ""
+
+msgid "Please indicate your preference for the guit-members mailing list."
+msgstr ""
+
+msgid "Consent to data processing is mandatory."
+msgstr ""
+
+#: includes/class-file-upload.php
+msgid "File upload failed."
+msgstr ""
+
+msgid "The file exceeds the maximum size of 10 MB."
+msgstr ""
+
+msgid "Only PDF, JPG and PNG files are allowed."
+msgstr ""
+
+msgid "Could not save the uploaded file."
+msgstr ""
+
+msgid "Unauthorized"
+msgstr ""
+
+msgid "Invalid nonce"
+msgstr ""
+
+msgid "File not found"
+msgstr ""
+
+#: includes/class-registration.php
+msgid "First Name"
+msgstr ""
+
+msgid "Last Name"
+msgstr ""
+
+#: includes/class-profile.php
+msgid "Journal Subscription"
+msgstr ""
+
+msgid "Status"
+msgstr ""
+
+msgid "Requested – pending approval"
+msgstr ""
+
+msgid "Not subscribed"
+msgstr ""
+
+msgid "Download"
+msgstr ""
+
+msgid "Upload a new file to replace the existing one."
+msgstr ""
+
+msgid "Deactivating the subscription will immediately disconnect this user from WordPress and revoke their access to the journal on OJS. Continue?"
+msgstr ""
+
+msgid "[%s] New journal subscription request"
+msgstr ""
+
+msgid "User \"%1$s\" (%2$s) has requested a journal subscription.\n\nReview pending requests: %3$s"
+msgstr ""
+
+msgid "User \"%1$s\" (%2$s) has requested a journal subscription from their profile.\n\nReview: %3$s"
+msgstr ""
+
+#: includes/class-subscription-fields.php
+msgid "Bank transfer receipt (PDF, JPG or PNG, max 10 MB)"
+msgstr ""
+
+msgid "A bank transfer receipt is required for subscription requests."
+msgstr ""
+
+#: includes/class-user-subscription-media.php
+msgid "Invalid upload request."
+msgstr ""
+
+#: includes/class-file-upload.php
+msgid "Storage handler is not available."
+msgstr ""
+
+#: includes/class-user-meta.php
+msgid "Subscription documents"
+msgstr ""
+
+msgid "Open"
+msgstr ""
+
+msgid "Bank transfer receipt"
+msgstr ""
+
+msgid "Bonifico"
+msgstr ""
+
+msgid "Certificato"
+msgstr ""
+
+#: includes/class-profile.php
+msgid "Open in Media"
+msgstr ""
+
+msgid "View file"
+msgstr ""
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 0000000..6fa75de
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,48 @@
+=== WP OJS SSO Bridge ===
+Contributors: guitex
+Tags: ojs, sso, openid-connect, journal, subscription
+Requires at least: 6.0
+Tested up to: 6.8
+Requires PHP: 7.4
+Stable tag: 1.1.0
+License: GPL-2.0-or-later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+Bridges WordPress and Open Journal Systems (OJS) via OpenID Connect, managing journal subscription status as an OIDC claim.
+
+== Description ==
+
+WP OJS SSO Bridge turns your WordPress site into an OpenID Connect identity provider for OJS (Open Journal Systems). It allows you to:
+
+* Manage a boolean "subscription active" flag per user from the WordPress admin
+* Expose the subscription status as a custom OIDC claim
+* Add a subscription request checkbox to the WordPress registration form
+* Collect membership data (personal details, membership type, consents) during registration or from the user profile
+* Notify admins when users request subscriptions
+* Bulk-activate or deactivate subscriptions from the users list
+* Store the bank transfer receipt and the student certificate in the Media Library under `wp-content/uploads/file-utenti/{user}/bonifico` and `.../certificato` (direct URLs follow normal WordPress upload visibility; restrict at the web server if you need non-public files)
+
+**Requires** the [OpenID Connect Server](https://wordpress.org/plugins/openid-connect-server/) plugin by Automattic.
+
+With the site language set to **Italiano** (Impostazioni → Generale), the OIDC *Authorize* / consent page (Ciao [ … ] Vuoi accedere… / Autorizza) is shown in Italian via this plugin; the upstream plugin ships those strings in English.
+
+== Installation ==
+
+1. Install and activate the "OpenID Connect Server" plugin by Automattic.
+2. Upload the `wp-ojs-sso-bridge` folder to `/wp-content/plugins/`.
+3. Activate the plugin through the 'Plugins' menu.
+4. Go to Settings > OJS SSO Bridge and configure your OJS client credentials.
+5. Generate RSA keys for the OIDC server: `wp eval 'OIDC\generateKeys();'`
+
+== Changelog ==
+
+= 1.1.0 =
+* Ricevuta bonifico obbligatoria per la richiesta di sottoscrizione: upload in Media in `file-utenti/{slug-utente}/bonifico`.
+* Documento studente: stesso meccanismo in `file-utenti/{slug-utente}/certificato` (i vecchi file sotto `ojs-sso-bridge/{id}/` restano scaricabili fino a sostituzione).
+* Colonna "Subscription documents" nella lista utenti con link a bonifico e certificato (o trattino se mancante).
+
+= 1.0.1 =
+* Testi della schermata di consenso OpenID Connect Server (Autorizza, messaggio per client OJS, titolo “OIDC Connect”) in italiano quando la lingua del sito inizia con `it`.
+
+= 1.0.0 =
+* Initial release.
diff --git a/uninstall.php b/uninstall.php
new file mode 100644
index 0000000..77dc24f
--- /dev/null
+++ b/uninstall.php
@@ -0,0 +1,42 @@
+isDir() ) {
+ rmdir( $file->getRealPath() );
+ } else {
+ unlink( $file->getRealPath() );
+ }
+ }
+ rmdir( $plugin_upload );
+ }
+}
+
+delete_option( 'wp_ojs_sso_bridge_settings' );
diff --git a/wp-ojs-sso-bridge.php b/wp-ojs-sso-bridge.php
new file mode 100644
index 0000000..0dabb44
--- /dev/null
+++ b/wp-ojs-sso-bridge.php
@@ -0,0 +1,32 @@
+