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

%s

', + 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 ''; + + wp_nonce_field( 'wp_ojs_sso_save_profile', '_wp_ojs_sso_profile_nonce' ); + + // Status display + echo ''; + + if ( ! $requested && ! $active ) { + // Offer to subscribe + $checkbox_text = WP_OJS_SSO_Plugin::get_option( + 'registration_checkbox_text', + __( 'I want to subscribe to the journal', 'wp-ojs-sso-bridge' ) + ); + echo ''; + } + + // Show subscription fields (editable) if requested or active + if ( $requested || $active ) { + $this->render_subscription_data_rows( $user, false ); + } else { + // Hidden block that shows when checkbox is toggled + echo ''; + echo ''; + echo ''; // close outer table cleanly + } + + 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 ''; + + wp_nonce_field( 'wp_ojs_sso_save_meta', '_wp_ojs_sso_nonce' ); + + // Active toggle (admin only) + $label = WP_OJS_SSO_Plugin::get_option( 'user_meta_label', __( 'OJS Subscription Active', 'wp-ojs-sso-bridge' ) ); + echo ''; + + $this->render_subscription_data_rows( $user, true ); + + echo ''; + } + + private function render_subscription_data_rows( $user, $is_admin ) { + $fields = WP_OJS_SSO_Subscription_Fields::get_fields(); + + // First/last name + echo ''; + printf( '', esc_attr( $user->first_name ) ); + echo ''; + + echo ''; + 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 ''; + + 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 '
'; + + 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( + '
', + esc_attr( $key ), + esc_attr( $opt_val ), + checked( $value, (string) $opt_val, false ), + esc_html( $opt_label ) + ); + } + break; + + case 'checkbox': + printf( + '', + 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 @@ +