diff --git a/.github/assets/screenshots/settings-button.png b/.github/assets/screenshots/settings-button.png index a08a5ad..4cfdf1a 100644 Binary files a/.github/assets/screenshots/settings-button.png and b/.github/assets/screenshots/settings-button.png differ diff --git a/.github/assets/screenshots/settings-connection.png b/.github/assets/screenshots/settings-connection.png index ac0bd92..6db4b2d 100644 Binary files a/.github/assets/screenshots/settings-connection.png and b/.github/assets/screenshots/settings-connection.png differ diff --git a/.github/assets/screenshots/settings-security.png b/.github/assets/screenshots/settings-security.png index a0afedb..8c162de 100644 Binary files a/.github/assets/screenshots/settings-security.png and b/.github/assets/screenshots/settings-security.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2151d51..739ce8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ All notable changes to this project are documented in this file. The format foll ### Added - "Sign in with Microsoft" button on `wp-login.php` (OpenID Connect authorization code flow with PKCE). - Matching of existing WordPress users by e-mail address (optional UPN fallback), no user provisioning. -- Settings screen (Settings → M365 Login) with connection, button and security tabs, live button preview, colour presets, media-library icon picker, redirect-URI copy button and tenant connectivity test. +- Settings screen (own top-level menu entry "M365 Login") with connection, button and security tabs, live button preview, colour presets, media-library icon picker, redirect-URI copy button and tenant connectivity test. - ID token verification against Microsoft's JWKS (RS256, issuer, audience, tenant, expiry, nonce). - Encrypted client secret storage (AES-256-GCM). - Certificate based client authentication (RFC 7523 `private_key_jwt`): one-click generation of a 3072-bit RSA key pair with self-signed certificate, `.cer` download, own PEM upload, expiry display; step-by-step guides for both methods in the settings. diff --git a/README.md b/README.md index 7ada872..470cb47 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,12 @@ | --- | --- | | ![Login-Seite mit Microsoft-Button](.github/assets/screenshots/login.png) | ![Login-Seite im Nur-Button-Modus](.github/assets/screenshots/login-button-only.png) | -| Einstellungen – Verbindung | Einstellungen – Button | +| M365 Login – Verbindung | M365 Login – Button | | --- | --- | | ![Tab Verbindung](.github/assets/screenshots/settings-connection.png) | ![Tab Button mit Live-Vorschau](.github/assets/screenshots/settings-button.png) |
-Einstellungen – Sicherheit (Gruppen-Auswahl, Nur-Button-Modus, Fallback-Link) +M365 Login – Sicherheit (Gruppen-Auswahl, Nur-Button-Modus, Fallback-Link) ![Tab Sicherheit](.github/assets/screenshots/settings-security.png) @@ -136,7 +136,7 @@ Alternativ den Repo-Inhalt als Ordner `m365-login` nach `wp-content/plugins/` ko
Schritt für Schritt (ca. 5 Minuten) -1. **Redirect-URI kopieren.** In WordPress *Einstellungen → M365 Login* öffnen; die URI steht in der Seitenleiste +1. **Redirect-URI kopieren.** In WordPress den Menüpunkt *M365 Login* öffnen; die URI steht in der Seitenleiste (`https://deine-seite.tld/m365-login/callback`, bei einfachen Permalinks `https://deine-seite.tld/?m365-login=callback`). 2. **App registrieren.** [Microsoft Entra Admin Center](https://entra.microsoft.com/) → *App-Registrierungen → Neue Registrierung* - Name: z. B. „WordPress Login“ @@ -177,7 +177,7 @@ Wichtig: Jeder Benutzer, der sich per Microsoft anmelden soll, braucht in WordPr ## Einstellungen im Backend -*Einstellungen → M365 Login* – drei Tabs, ein Formular, ein Speichern-Button. +Menüpunkt *M365 Login* – drei Tabs (auch als Untermenüs erreichbar), ein Formular, ein Speichern-Button. ### Verbindung diff --git a/assets/js/admin.js b/assets/js/admin.js index a977db2..a8cbb45 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -18,18 +18,36 @@ try { window.localStorage.setItem( 'm365LoginTab', name ); } catch ( e ) {} + // Keep the URL (and the post-save redirect) on this tab. + var $referer = $( '#m365-referer' ); + if ( $referer.length && window.history && window.history.replaceState ) { + var url = new URL( window.location.href ); + if ( 'connection' === name ) { + url.searchParams.delete( 'tab' ); + } else { + url.searchParams.set( 'tab', name ); + } + url.searchParams.delete( 'settings-updated' ); + window.history.replaceState( null, '', url.toString() ); + $referer.val( url.pathname + url.search ); + } } $tabs.on( 'click', function () { activate( $( this ).data( 'tab' ) ); } ); - try { - var saved = window.localStorage.getItem( 'm365LoginTab' ); - if ( saved && $tabs.filter( '[data-tab="' + saved + '"]' ).length ) { - activate( saved ); - } - } catch ( e ) {} + var initial = $( '.m365-admin__tabs' ).data( 'initial-tab' ); + if ( initial && $tabs.filter( '[data-tab="' + initial + '"]' ).length ) { + activate( initial ); + } else { + try { + var saved = window.localStorage.getItem( 'm365LoginTab' ); + if ( saved && $tabs.filter( '[data-tab="' + saved + '"]' ).length ) { + activate( saved ); + } + } catch ( e ) {} + } // Jump to the tab that contains a validation error. var $error = $( '.settings-error' ).first(); diff --git a/includes/class-m365-login-admin.php b/includes/class-m365-login-admin.php index 81afba3..8f74288 100644 --- a/includes/class-m365-login-admin.php +++ b/includes/class-m365-login-admin.php @@ -18,6 +18,7 @@ class M365_Login_Admin { const AJAX_GROUPS = 'm365_login_search_groups'; const AJAX_CERT = 'm365_login_certificate'; const POST_CERT = 'm365_login_download_cert'; + const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbD0iYmxhY2siIGQ9Ik0yIDJoNy41djcuNUgyek0xMC41IDJIMTh2Ny41aC03LjV6TTIgMTAuNWg3LjVWMThIMnpNMTAuNSAxMC41SDE4VjE4aC03LjV6Ii8+PC9zdmc+'; const NONCE_TEST = 'm365_login_test'; /** @@ -69,19 +70,85 @@ class M365_Login_Admin { add_action( 'admin_post_' . self::POST_CERT, array( $this, 'download_certificate' ) ); add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) ); add_action( 'admin_notices', array( $this, 'setup_notice' ) ); + add_filter( 'submenu_file', array( $this, 'highlight_submenu' ) ); } /** - * Adds the menu entry under Settings. + * Adds a top-level menu entry with one submenu per tab. */ public function menu() { - $this->hook = add_options_page( + $this->hook = add_menu_page( __( 'M365 Login', 'm365-login' ), __( 'M365 Login', 'm365-login' ), 'manage_options', self::PAGE, - array( $this, 'render' ) + array( $this, 'render' ), + self::MENU_ICON, + 81 ); + + foreach ( self::tabs() as $tab => $label ) { + add_submenu_page( + self::PAGE, + $label . ' – ' . __( 'M365 Login', 'm365-login' ), + $label, + 'manage_options', + 'connection' === $tab ? self::PAGE : 'admin.php?page=' . self::PAGE . '&tab=' . $tab + ); + } + } + + /** + * Tab slugs and labels. + * + * @return string[] + */ + public static function tabs() { + return array( + 'connection' => __( 'Connection', 'm365-login' ), + 'button' => __( 'Button', 'm365-login' ), + 'security' => __( 'Security', 'm365-login' ), + ); + } + + /** + * Currently requested tab (from ?tab=). + * + * @return string + */ + private function current_tab() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only UI state. + $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; + return array_key_exists( $tab, self::tabs() ) ? $tab : ''; + } + + /** + * Highlights the submenu entry matching the requested tab. + * + * @param string|null $submenu_file Current submenu file. + * @return string|null + */ + public function highlight_submenu( $submenu_file ) { + $screen = get_current_screen(); + if ( ! $screen || $this->hook !== $screen->id ) { + return $submenu_file; + } + $tab = $this->current_tab(); + if ( '' === $tab || 'connection' === $tab ) { + return self::PAGE; + } + return 'admin.php?page=' . self::PAGE . '&tab=' . $tab; + } + + /** + * URL of the settings screen (optionally a specific tab). + * + * @param string $tab Tab slug. + * @return string + */ + public static function url( $tab = '' ) { + $url = admin_url( 'admin.php?page=' . self::PAGE ); + return '' === $tab ? $url : add_query_arg( 'tab', $tab, $url ); } /** @@ -116,7 +183,7 @@ class M365_Login_Admin { printf( '

%s %s

', esc_html__( 'M365 Login is active but not connected to Microsoft Entra ID yet.', 'm365-login' ), - esc_url( admin_url( 'options-general.php?page=' . self::PAGE ) ), + esc_url( self::url() ), esc_html__( 'Open the settings', 'm365-login' ) ); } @@ -322,13 +389,16 @@ class M365_Login_Admin { + +
+ -