' ).text( i18n.syncRunning ) );
+
+ $.post( cfg.ajaxUrl, { action: cfg.syncAction, nonce: cfg.nonce, op: op } ).done( function ( res ) {
+ if ( res && res.success ) {
+ $report.html( res.data.html );
+ } else {
+ $report.html( $( '
' ).text( ( res && res.data && res.data.message ) || i18n.syncFailed ) );
+ }
} ).fail( function () {
- $groupResults.addClass( 'is-error' ).html( '
' ).text( i18n.syncFailed ) );
+ } ).always( function () {
+ $( '.m365-sync-run' ).prop( 'disabled', false );
} );
- }
-
- function buildResult( g ) {
- var $row = $( '
diff --git a/includes/class-m365-login-auth.php b/includes/class-m365-login-auth.php
index 757d9b3..810e99a 100644
--- a/includes/class-m365-login-auth.php
+++ b/includes/class-m365-login-auth.php
@@ -441,6 +441,10 @@ class M365_Login_Auth {
$this->fail( 'no_user' );
}
+ if ( M365_Login_Sync::disabled_info( $user->ID ) ) {
+ $this->fail( 'account_disabled' );
+ }
+
$oid = isset( $claims['oid'] ) && is_string( $claims['oid'] ) ? strtolower( $claims['oid'] ) : '';
// Entra group restriction.
@@ -857,6 +861,7 @@ class M365_Login_Auth {
'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
'too_many_attempts' => __( 'Too many sign-in attempts from your connection. Please wait a few minutes and try again.', 'm365-login' ),
+ 'account_disabled' => __( 'This account has been deactivated.', 'm365-login' ),
);
}
diff --git a/includes/class-m365-login-graph.php b/includes/class-m365-login-graph.php
index ba1f6e5..027c485 100644
--- a/includes/class-m365-login-graph.php
+++ b/includes/class-m365-login-graph.php
@@ -8,7 +8,7 @@
defined( 'ABSPATH' ) || exit;
/**
- * Obtains app-only tokens via client credentials and queries groups.
+ * Obtains app-only tokens via client credentials and queries users and groups.
*/
class M365_Login_Graph {
@@ -98,6 +98,63 @@ class M365_Login_Graph {
return (string) $body['access_token'];
}
+ /**
+ * Performs an authenticated Graph request and returns the raw HTTP response.
+ *
+ * Retries a few times when Microsoft throttles (HTTP 429) or is briefly unavailable (503/504).
+ *
+ * @param string $method HTTP method.
+ * @param string $path Path relative to the v1.0 base (with query string) or an absolute Graph URL (paging links).
+ * @param array|null $json JSON body for POST requests.
+ * @param array $headers Extra headers.
+ * @return array|WP_Error Response array from wp_remote_request().
+ */
+ private function raw_request( $method, $path, $json = null, $headers = array() ) {
+ $url = 0 === strpos( $path, self::GRAPH_BASE . '/' ) ? $path : self::GRAPH_BASE . $path;
+ if ( 0 !== strpos( $url, self::GRAPH_BASE . '/' ) ) {
+ return new WP_Error( 'graph_bad_url', 'Refusing to call a non-Graph URL.' );
+ }
+
+ for ( $attempt = 1; ; $attempt++ ) {
+ $token = $this->app_token();
+ if ( is_wp_error( $token ) ) {
+ return $token;
+ }
+
+ $args = array(
+ 'method' => $method,
+ 'timeout' => self::HTTP_TIMEOUT,
+ 'headers' => array_merge(
+ array(
+ 'Authorization' => 'Bearer ' . $token,
+ 'Accept' => 'application/json',
+ ),
+ $headers
+ ),
+ );
+ if ( null !== $json ) {
+ $args['headers']['Content-Type'] = 'application/json';
+ $args['body'] = wp_json_encode( $json );
+ }
+
+ $response = wp_remote_request( $url, $args );
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $code = (int) wp_remote_retrieve_response_code( $response );
+ if ( 401 === $code ) {
+ $this->flush_token();
+ }
+ if ( $attempt < 4 && in_array( $code, array( 429, 503, 504 ), true ) ) {
+ $wait = (int) wp_remote_retrieve_header( $response, 'retry-after' );
+ sleep( max( 1, min( 10, $wait > 0 ? $wait : $attempt * 2 ) ) );
+ continue;
+ }
+ return $response;
+ }
+ }
+
/**
* Performs an authenticated Graph request.
*
@@ -105,31 +162,10 @@ class M365_Login_Graph {
* @param string $path Path relative to the v1.0 base (with query string).
* @param array|null $json JSON body for POST requests.
* @param array $headers Extra headers.
- * @return array|WP_Error Decoded JSON.
+ * @return array|WP_Error Decoded JSON. Errors carry array( 'status' => HTTP code ) as data.
*/
private function request( $method, $path, $json = null, $headers = array() ) {
- $token = $this->app_token();
- if ( is_wp_error( $token ) ) {
- return $token;
- }
-
- $args = array(
- 'method' => $method,
- 'timeout' => self::HTTP_TIMEOUT,
- 'headers' => array_merge(
- array(
- 'Authorization' => 'Bearer ' . $token,
- 'Accept' => 'application/json',
- ),
- $headers
- ),
- );
- if ( null !== $json ) {
- $args['headers']['Content-Type'] = 'application/json';
- $args['body'] = wp_json_encode( $json );
- }
-
- $response = wp_remote_request( self::GRAPH_BASE . $path, $args );
+ $response = $this->raw_request( $method, $path, $json, $headers );
if ( is_wp_error( $response ) ) {
return $response;
}
@@ -137,18 +173,156 @@ class M365_Login_Graph {
$code = (int) wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
- if ( 401 === $code ) {
- $this->flush_token();
- }
if ( $code < 200 || $code >= 300 || ! is_array( $body ) ) {
- $graph_code = isset( $body['error']['code'] ) ? (string) $body['error']['code'] : 'HTTP ' . $code;
- $message = isset( $body['error']['message'] ) ? (string) $body['error']['message'] : '';
- return new WP_Error( 'graph_' . sanitize_key( $graph_code ), $graph_code . ( $message ? ': ' . $message : '' ) );
+ return $this->error_from( $code, $body );
}
return $body;
}
+ /**
+ * Builds a WP_Error from a failed Graph response.
+ *
+ * @param int $code HTTP status.
+ * @param array|null $body Decoded body.
+ * @return WP_Error
+ */
+ private function error_from( $code, $body ) {
+ $graph_code = isset( $body['error']['code'] ) ? (string) $body['error']['code'] : 'HTTP ' . $code;
+ $message = isset( $body['error']['message'] ) ? (string) $body['error']['message'] : '';
+ return new WP_Error( 'graph_' . sanitize_key( $graph_code ), $graph_code . ( $message ? ': ' . $message : '' ), array( 'status' => (int) $code ) );
+ }
+
+ /**
+ * Whether a Graph error means "object does not exist".
+ *
+ * @param WP_Error $error Error.
+ * @return bool
+ */
+ public static function is_not_found( $error ) {
+ $data = $error->get_error_data();
+ return is_array( $data ) && isset( $data['status'] ) && 404 === (int) $data['status'];
+ }
+
+ /**
+ * Follows @odata.nextLink until every page of a collection is read.
+ *
+ * @param string $path First page (relative path).
+ * @param array $headers Extra headers.
+ * @return array[]|WP_Error All items.
+ */
+ private function collect( $path, $headers = array() ) {
+ $items = array();
+ $next = $path;
+ $pages = 0;
+ while ( '' !== $next ) {
+ if ( ++$pages > 1000 ) {
+ return new WP_Error( 'graph_paging', 'Too many result pages.' );
+ }
+ $result = $this->request( 'GET', $next, null, $headers );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ if ( isset( $result['value'] ) && is_array( $result['value'] ) ) {
+ foreach ( $result['value'] as $item ) {
+ if ( is_array( $item ) && ! empty( $item['id'] ) ) {
+ $items[] = $item;
+ }
+ }
+ }
+ $next = isset( $result['@odata.nextLink'] ) && is_string( $result['@odata.nextLink'] ) ? $result['@odata.nextLink'] : '';
+ }
+ return $items;
+ }
+
+ /**
+ * Lists every user of the tenant.
+ *
+ * @param string[] $select Properties to read.
+ * @return array[]|WP_Error
+ */
+ public function list_users( $select ) {
+ return $this->collect( '/users?$select=' . rawurlencode( implode( ',', $select ) ) . '&$top=999' );
+ }
+
+ /**
+ * Lists the users that are (directly or through nested groups) members of a group.
+ *
+ * @param string $group_id Group object ID.
+ * @param string[] $select Properties to read.
+ * @return array[]|WP_Error
+ */
+ public function list_group_users( $group_id, $select ) {
+ if ( ! M365_Login_Settings::is_guid( $group_id ) ) {
+ return new WP_Error( 'graph_bad_group', 'Invalid group object ID.' );
+ }
+ return $this->collect(
+ '/groups/' . rawurlencode( strtolower( $group_id ) ) . '/transitiveMembers/microsoft.graph.user?$select=' . rawurlencode( implode( ',', $select ) ) . '&$top=999&$count=true',
+ array( 'ConsistencyLevel' => 'eventual' )
+ );
+ }
+
+ /**
+ * Reads a single user.
+ *
+ * @param string $oid User object ID.
+ * @param string[] $select Properties to read.
+ * @return array|WP_Error WP_Error with status 404 when the user does not exist (anymore).
+ */
+ public function get_user( $oid, $select ) {
+ if ( ! M365_Login_Settings::is_guid( $oid ) ) {
+ return new WP_Error( 'graph_bad_oid', 'Invalid user object ID.' );
+ }
+ return $this->request( 'GET', '/users/' . rawurlencode( strtolower( $oid ) ) . '?$select=' . rawurlencode( implode( ',', $select ) ) );
+ }
+
+ /**
+ * Metadata of a user's profile photo (prefers the 240×240 rendition).
+ *
+ * @param string $oid User object ID.
+ * @return array|null|WP_Error array( 'path' => photo path, 'etag' => string ), null when the user has no photo.
+ */
+ public function photo_info( $oid ) {
+ if ( ! M365_Login_Settings::is_guid( $oid ) ) {
+ return new WP_Error( 'graph_bad_oid', 'Invalid user object ID.' );
+ }
+ $base = '/users/' . rawurlencode( strtolower( $oid ) );
+ foreach ( array( $base . '/photos/240x240', $base . '/photo' ) as $path ) {
+ $meta = $this->request( 'GET', $path );
+ if ( is_wp_error( $meta ) ) {
+ if ( self::is_not_found( $meta ) ) {
+ continue;
+ }
+ return $meta;
+ }
+ $etag = isset( $meta['@odata.mediaEtag'] ) ? (string) $meta['@odata.mediaEtag'] : '';
+ return array(
+ 'path' => $path,
+ 'etag' => '' !== $etag ? $etag : md5( (string) wp_json_encode( $meta ) ),
+ );
+ }
+ return null;
+ }
+
+ /**
+ * Downloads photo bytes.
+ *
+ * @param string $path Photo path returned by photo_info().
+ * @return string|WP_Error Binary image data.
+ */
+ public function photo_bytes( $path ) {
+ $response = $this->raw_request( 'GET', $path . '/$value', null, array( 'Accept' => 'image/*' ) );
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+ $code = (int) wp_remote_retrieve_response_code( $response );
+ $body = wp_remote_retrieve_body( $response );
+ if ( 200 !== $code ) {
+ return $this->error_from( $code, json_decode( $body, true ) );
+ }
+ return $body;
+ }
+
/**
* Searches groups by display name.
*
diff --git a/includes/class-m365-login-settings.php b/includes/class-m365-login-settings.php
index a4b8bd6..5852f00 100644
--- a/includes/class-m365-login-settings.php
+++ b/includes/class-m365-login-settings.php
@@ -34,36 +34,50 @@ class M365_Login_Settings {
public function defaults() {
return array(
// Connection.
- 'tenant_id' => '',
- 'client_id' => '',
- 'client_secret' => '', // Stored encrypted.
- 'auth_method' => 'secret', // 'secret' or 'certificate'.
- 'cert_private_key' => '', // PEM, stored encrypted.
- 'cert_certificate' => '', // PEM (public).
- 'prompt' => 'select_account',
+ 'tenant_id' => '',
+ 'client_id' => '',
+ 'client_secret' => '', // Stored encrypted.
+ 'auth_method' => 'secret', // 'secret' or 'certificate'.
+ 'cert_private_key' => '', // PEM, stored encrypted.
+ 'cert_certificate' => '', // PEM (public).
+ 'prompt' => 'select_account',
// Security / matching.
- 'upn_fallback' => 1,
- 'bind_oid' => 1,
- 'allowed_domains' => '',
- 'allowed_groups' => array(), // id => display name.
- 'remember_me' => 0,
+ 'upn_fallback' => 1,
+ 'bind_oid' => 1,
+ 'allowed_domains' => '',
+ 'allowed_groups' => array(), // id => display name.
+ 'remember_me' => 0,
// Button-only mode.
- 'button_only' => 0,
- 'fallback_key' => '',
+ 'button_only' => 0,
+ 'fallback_key' => '',
// Button appearance.
- 'button_text' => __( 'Sign in with Microsoft', 'm365-login' ),
- 'button_icon' => '', // Empty = bundled Microsoft logo.
- 'button_show_icon' => 1,
- 'button_bg' => '#2f2f2f',
- 'button_bg_hover' => '#1a1a1a',
- 'button_color' => '#ffffff',
- 'button_border' => '#2f2f2f',
- 'button_radius' => 4,
- 'button_position' => 'below',
- 'divider_text' => __( 'or', 'm365-login' ),
+ 'button_text' => __( 'Sign in with Microsoft', 'm365-login' ),
+ 'button_icon' => '', // Empty = bundled Microsoft logo.
+ 'button_show_icon' => 1,
+ 'button_bg' => '#2f2f2f',
+ 'button_bg_hover' => '#1a1a1a',
+ 'button_color' => '#ffffff',
+ 'button_border' => '#2f2f2f',
+ 'button_radius' => 4,
+ 'button_position' => 'below',
+ 'divider_text' => __( 'or', 'm365-login' ),
// Custom login pages.
- 'custom_login_url' => '',
- 'inject_form' => 1, // Add the button to wp_login_form() output.
+ 'custom_login_url' => '',
+ 'inject_form' => 1, // Add the button to wp_login_form() output.
+ // User sync.
+ 'sync_enabled' => 0, // Scheduled sync via WP-Cron.
+ 'sync_interval' => 'daily',
+ 'sync_guests' => 0,
+ 'sync_scope_groups' => array(), // id => display name; empty = whole tenant.
+ 'sync_default_role' => 'subscriber',
+ 'sync_role_map' => array(), // id => array( 'name' => .., 'role' => .. ), in priority order.
+ 'sync_role_mode' => 'add', // 'add' (extra roles) or 'replace' (first match replaces the default role).
+ 'sync_manage_existing' => 0, // Also manage roles of accounts that existed before the sync.
+ 'sync_attributes' => array( 'displayName', 'givenName', 'surname' ),
+ 'sync_disabled_action' => 'disable', // Account disabled in Microsoft 365: none|disable|delete.
+ 'sync_deleted_action' => 'disable', // Account deleted in Microsoft 365: none|disable|delete.
+ 'sync_scope_action' => 'none', // Removed from the sync groups: none|disable|delete.
+ 'sync_reassign' => 0, // User ID that receives content of deleted users.
);
}
@@ -80,6 +94,13 @@ class M365_Login_Settings {
return $this->cache;
}
+ /**
+ * Drops the cached settings (after the option was written).
+ */
+ public function flush() {
+ $this->cache = null;
+ }
+
/**
* Returns a single setting.
*
@@ -309,7 +330,47 @@ class M365_Login_Settings {
* @return array
*/
public function allowed_groups() {
- $raw = $this->get( 'allowed_groups', array() );
+ return self::guid_map( $this->get( 'allowed_groups', array() ) );
+ }
+
+ /**
+ * Groups that limit the user sync (lowercase GUID => name); empty = whole tenant.
+ *
+ * @return array
+ */
+ public function sync_scope_groups() {
+ return self::guid_map( $this->get( 'sync_scope_groups', array() ) );
+ }
+
+ /**
+ * Group → role mapping in priority order.
+ *
+ * @return array lowercase GUID => array( 'name' => string, 'role' => string ).
+ */
+ public function sync_role_map() {
+ $raw = $this->get( 'sync_role_map', array() );
+ $out = array();
+ if ( is_array( $raw ) ) {
+ foreach ( $raw as $id => $row ) {
+ $id = strtolower( (string) $id );
+ if ( self::is_guid( $id ) && is_array( $row ) && ! empty( $row['role'] ) ) {
+ $out[ $id ] = array(
+ 'name' => isset( $row['name'] ) ? (string) $row['name'] : $id,
+ 'role' => (string) $row['role'],
+ );
+ }
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Keeps GUID keys (lowercased) of an id => name array.
+ *
+ * @param mixed $raw Stored value.
+ * @return array
+ */
+ private static function guid_map( $raw ) {
$out = array();
if ( is_array( $raw ) ) {
foreach ( $raw as $id => $name ) {
@@ -449,8 +510,8 @@ class M365_Login_Settings {
// Certificate: keep the stored pair unless a new one is pasted or removal is requested.
$out['cert_private_key'] = $current['cert_private_key'];
$out['cert_certificate'] = $current['cert_certificate'];
- $pasted_key = isset( $input['cert_key_pem'] ) ? trim( (string) wp_unslash( $input['cert_key_pem'] ) ) : '';
- $pasted_cert = isset( $input['cert_cert_pem'] ) ? trim( (string) wp_unslash( $input['cert_cert_pem'] ) ) : '';
+ $pasted_key = isset( $input['cert_key_pem'] ) ? trim( (string) wp_unslash( $input['cert_key_pem'] ) ) : '';
+ $pasted_cert = isset( $input['cert_cert_pem'] ) ? trim( (string) wp_unslash( $input['cert_cert_pem'] ) ) : '';
if ( ! empty( $input['cert_remove'] ) ) {
$out['cert_private_key'] = '';
$out['cert_certificate'] = '';
@@ -485,26 +546,12 @@ class M365_Login_Settings {
$out['bind_oid'] = empty( $input['bind_oid'] ) ? 0 : 1;
$out['remember_me'] = empty( $input['remember_me'] ) ? 0 : 1;
- $domains = isset( $input['allowed_domains'] ) ? sanitize_textarea_field( wp_unslash( $input['allowed_domains'] ) ) : '';
- $domains = preg_replace( '/[^a-z0-9.\-@,;\s]/i', '', $domains );
+ $domains = isset( $input['allowed_domains'] ) ? sanitize_textarea_field( wp_unslash( $input['allowed_domains'] ) ) : '';
+ $domains = preg_replace( '/[^a-z0-9.\-@,;\s]/i', '', $domains );
$out['allowed_domains'] = trim( (string) $domains );
// Allowed groups: GUID => name.
- $groups = array();
- if ( ! empty( $input['allowed_groups'] ) && is_array( $input['allowed_groups'] ) ) {
- foreach ( $input['allowed_groups'] as $id => $name ) {
- $id = strtolower( trim( sanitize_text_field( wp_unslash( (string) $id ) ) ) );
- if ( ! self::is_guid( $id ) ) {
- continue;
- }
- $name = sanitize_text_field( wp_unslash( (string) $name ) );
- $groups[ $id ] = '' === $name ? $id : mb_substr( $name, 0, 120 );
- if ( count( $groups ) >= 100 ) {
- break;
- }
- }
- }
- $out['allowed_groups'] = $groups;
+ $out['allowed_groups'] = self::sanitize_group_list( isset( $input['allowed_groups'] ) ? $input['allowed_groups'] : array() );
// Button-only mode + fallback key.
$out['button_only'] = empty( $input['button_only'] ) ? 0 : 1;
@@ -551,11 +598,114 @@ class M365_Login_Settings {
$divider = isset( $input['divider_text'] ) ? sanitize_text_field( wp_unslash( $input['divider_text'] ) ) : '';
$out['divider_text'] = mb_substr( $divider, 0, 40 );
+ $out = $this->sanitize_sync( $input, $out );
+
$this->cache = null;
return $out;
}
+ /**
+ * Sanitises the user sync settings.
+ *
+ * @param array $input Raw input.
+ * @param array $out Settings sanitised so far.
+ * @return array
+ */
+ private function sanitize_sync( $input, $out ) {
+ $defaults = $this->defaults();
+
+ $out['sync_enabled'] = empty( $input['sync_enabled'] ) ? 0 : 1;
+ $out['sync_guests'] = empty( $input['sync_guests'] ) ? 0 : 1;
+ $out['sync_manage_existing'] = empty( $input['sync_manage_existing'] ) ? 0 : 1;
+
+ $interval = isset( $input['sync_interval'] ) ? sanitize_key( $input['sync_interval'] ) : '';
+ $out['sync_interval'] = in_array( $interval, array( 'hourly', 'twicedaily', 'daily' ), true ) ? $interval : $defaults['sync_interval'];
+
+ $mode = isset( $input['sync_role_mode'] ) ? sanitize_key( $input['sync_role_mode'] ) : '';
+ $out['sync_role_mode'] = in_array( $mode, array( 'add', 'replace' ), true ) ? $mode : $defaults['sync_role_mode'];
+
+ $role = isset( $input['sync_default_role'] ) ? sanitize_key( $input['sync_default_role'] ) : '';
+ $out['sync_default_role'] = '' !== $role && get_role( $role ) ? $role : $defaults['sync_default_role'];
+
+ $out['sync_scope_groups'] = self::sanitize_group_list( isset( $input['sync_scope_groups'] ) ? $input['sync_scope_groups'] : array() );
+
+ $map = array();
+ if ( ! empty( $input['sync_role_map'] ) && is_array( $input['sync_role_map'] ) ) {
+ foreach ( $input['sync_role_map'] as $id => $row ) {
+ $id = strtolower( trim( sanitize_text_field( wp_unslash( (string) $id ) ) ) );
+ if ( ! self::is_guid( $id ) || ! is_array( $row ) ) {
+ continue;
+ }
+ $map_role = isset( $row['role'] ) ? sanitize_key( $row['role'] ) : '';
+ if ( '' === $map_role || ! get_role( $map_role ) ) {
+ continue;
+ }
+ $name = isset( $row['name'] ) ? sanitize_text_field( wp_unslash( (string) $row['name'] ) ) : '';
+ $map[ $id ] = array(
+ 'name' => '' === $name ? $id : mb_substr( $name, 0, 120 ),
+ 'role' => $map_role,
+ );
+ if ( count( $map ) >= 100 ) {
+ break;
+ }
+ }
+ }
+ $out['sync_role_map'] = $map;
+
+ $attributes = array();
+ if ( ! empty( $input['sync_attributes'] ) && is_array( $input['sync_attributes'] ) ) {
+ $known = array_keys( M365_Login_Sync::attributes() );
+ foreach ( $input['sync_attributes'] as $attribute ) {
+ $attribute = sanitize_text_field( wp_unslash( (string) $attribute ) );
+ if ( in_array( $attribute, $known, true ) ) {
+ $attributes[] = $attribute;
+ }
+ }
+ }
+ $out['sync_attributes'] = array_values( array_unique( $attributes ) );
+
+ foreach ( array( 'sync_disabled_action', 'sync_deleted_action', 'sync_scope_action' ) as $key ) {
+ $action = isset( $input[ $key ] ) ? sanitize_key( $input[ $key ] ) : '';
+ $out[ $key ] = in_array( $action, array( 'none', 'disable', 'delete' ), true ) ? $action : $defaults[ $key ];
+ }
+
+ $reassign = isset( $input['sync_reassign'] ) ? absint( $input['sync_reassign'] ) : 0;
+ $out['sync_reassign'] = $reassign && get_userdata( $reassign ) ? $reassign : 0;
+
+ $deletes = in_array( 'delete', array( $out['sync_disabled_action'], $out['sync_deleted_action'], $out['sync_scope_action'] ), true );
+ if ( $deletes && ! $out['sync_reassign'] ) {
+ add_settings_error( M365_LOGIN_OPTION, 'sync_reassign', __( 'User sync: "Delete" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead.', 'm365-login' ), 'warning' );
+ }
+
+ return $out;
+ }
+
+ /**
+ * Sanitises a GUID => name list posted by a group picker.
+ *
+ * @param mixed $raw Raw input.
+ * @return array
+ */
+ private static function sanitize_group_list( $raw ) {
+ $groups = array();
+ if ( empty( $raw ) || ! is_array( $raw ) ) {
+ return $groups;
+ }
+ foreach ( $raw as $id => $name ) {
+ $id = strtolower( trim( sanitize_text_field( wp_unslash( (string) $id ) ) ) );
+ if ( ! self::is_guid( $id ) ) {
+ continue;
+ }
+ $name = sanitize_text_field( wp_unslash( (string) $name ) );
+ $groups[ $id ] = '' === $name ? $id : mb_substr( $name, 0, 120 );
+ if ( count( $groups ) >= 100 ) {
+ break;
+ }
+ }
+ return $groups;
+ }
+
/**
* Checks a GUID.
*
diff --git a/includes/class-m365-login-sync.php b/includes/class-m365-login-sync.php
new file mode 100644
index 0000000..5eb1022
--- /dev/null
+++ b/includes/class-m365-login-sync.php
@@ -0,0 +1,1578 @@
+settings = $settings;
+ $this->graph = $graph;
+
+ add_action( self::CRON_HOOK, array( $this, 'run_scheduled' ) );
+ add_action( 'init', array( $this, 'ensure_schedule' ) );
+ add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this, 'reschedule' ) );
+ add_action( 'add_option_' . M365_LOGIN_OPTION, array( $this, 'reschedule' ) );
+
+ // Deactivated accounts: no password, application password, cookie or Microsoft sign-in.
+ add_filter( 'authenticate', array( $this, 'block_disabled_login' ), 100, 1 );
+ add_filter( 'determine_current_user', array( $this, 'drop_disabled_session' ), 100 );
+
+ add_filter( 'pre_get_avatar_data', array( $this, 'avatar_data' ), 10, 2 );
+
+ if ( is_admin() ) {
+ add_filter( 'manage_users_columns', array( $this, 'users_column' ) );
+ add_filter( 'manage_users_custom_column', array( $this, 'users_column_value' ), 10, 3 );
+ add_filter( 'user_row_actions', array( $this, 'user_row_actions' ), 10, 2 );
+ add_action( 'admin_post_' . self::POST_STATE, array( $this, 'handle_user_state' ) );
+ add_action( 'show_user_profile', array( $this, 'profile_section' ) );
+ add_action( 'edit_user_profile', array( $this, 'profile_section' ) );
+ add_action( 'admin_notices', array( $this, 'user_state_notice' ) );
+ }
+
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ WP_CLI::add_command( 'm365-login sync', array( $this, 'cli' ) );
+ }
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Attributes */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Microsoft Graph user properties that can be copied into WordPress profiles.
+ *
+ * The target is either a WordPress user field (display_name, first_name, last_name,
+ * locale), a user meta key or "avatar" for the profile photo.
+ *
+ * @return array Graph property => array( 'label' => string, 'target' => string ).
+ */
+ public static function attributes() {
+ $attributes = array(
+ 'displayName' => array(
+ 'label' => __( 'Display name', 'm365-login' ),
+ 'target' => 'display_name',
+ ),
+ 'givenName' => array(
+ 'label' => __( 'First name', 'm365-login' ),
+ 'target' => 'first_name',
+ ),
+ 'surname' => array(
+ 'label' => __( 'Last name', 'm365-login' ),
+ 'target' => 'last_name',
+ ),
+ 'photo' => array(
+ 'label' => __( 'Profile photo (used as avatar)', 'm365-login' ),
+ 'target' => 'avatar',
+ ),
+ 'jobTitle' => array(
+ 'label' => __( 'Job title', 'm365-login' ),
+ 'target' => 'm365_job_title',
+ ),
+ 'department' => array(
+ 'label' => __( 'Department', 'm365-login' ),
+ 'target' => 'm365_department',
+ ),
+ 'companyName' => array(
+ 'label' => __( 'Company', 'm365-login' ),
+ 'target' => 'm365_company_name',
+ ),
+ 'officeLocation' => array(
+ 'label' => __( 'Office', 'm365-login' ),
+ 'target' => 'm365_office_location',
+ ),
+ 'employeeId' => array(
+ 'label' => __( 'Employee ID', 'm365-login' ),
+ 'target' => 'm365_employee_id',
+ ),
+ 'businessPhones' => array(
+ 'label' => __( 'Business phone', 'm365-login' ),
+ 'target' => 'm365_business_phone',
+ ),
+ 'mobilePhone' => array(
+ 'label' => __( 'Mobile phone', 'm365-login' ),
+ 'target' => 'm365_mobile_phone',
+ ),
+ 'streetAddress' => array(
+ 'label' => __( 'Street address', 'm365-login' ),
+ 'target' => 'm365_street_address',
+ ),
+ 'postalCode' => array(
+ 'label' => __( 'Postal code', 'm365-login' ),
+ 'target' => 'm365_postal_code',
+ ),
+ 'city' => array(
+ 'label' => __( 'City', 'm365-login' ),
+ 'target' => 'm365_city',
+ ),
+ 'state' => array(
+ 'label' => __( 'State / province', 'm365-login' ),
+ 'target' => 'm365_state',
+ ),
+ 'country' => array(
+ 'label' => __( 'Country', 'm365-login' ),
+ 'target' => 'm365_country',
+ ),
+ 'preferredLanguage' => array(
+ 'label' => __( 'Language (sets the admin language if installed)', 'm365-login' ),
+ 'target' => 'locale',
+ ),
+ );
+
+ /**
+ * Filters the Graph properties offered for the profile sync.
+ *
+ * Add entries as 'graphProperty' => array( 'label' => .., 'target' => 'meta_key' ).
+ *
+ * @param array $attributes Attributes.
+ */
+ return (array) apply_filters( 'm365_login_sync_attributes', $attributes );
+ }
+
+ /**
+ * Selected attributes that exist in the registry.
+ *
+ * @return array Graph property => target.
+ */
+ private function selected_attributes() {
+ $all = self::attributes();
+ $out = array();
+ foreach ( (array) $this->settings->get( 'sync_attributes', array() ) as $key ) {
+ if ( isset( $all[ $key ]['target'] ) ) {
+ $out[ $key ] = (string) $all[ $key ]['target'];
+ }
+ }
+ return $out;
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Scheduling */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Schedules the cron event if the sync is enabled but no event is queued.
+ */
+ public function ensure_schedule() {
+ if ( $this->settings->get( 'sync_enabled' ) && ! wp_next_scheduled( self::CRON_HOOK ) ) {
+ $this->reschedule();
+ }
+ }
+
+ /**
+ * (Re)creates or removes the cron event after the settings changed.
+ */
+ public function reschedule() {
+ $this->settings->flush();
+ wp_clear_scheduled_hook( self::CRON_HOOK );
+ if ( $this->settings->get( 'sync_enabled' ) ) {
+ wp_schedule_event( time() + 5 * MINUTE_IN_SECONDS, (string) $this->settings->get( 'sync_interval', 'daily' ), self::CRON_HOOK );
+ }
+ }
+
+ /**
+ * Removes the cron event (plugin deactivation).
+ */
+ public static function unschedule() {
+ wp_clear_scheduled_hook( self::CRON_HOOK );
+ }
+
+ /**
+ * Cron callback.
+ */
+ public function run_scheduled() {
+ if ( $this->settings->get( 'sync_enabled' ) ) {
+ $this->run( false, 'cron' );
+ }
+ }
+
+ /**
+ * WP-CLI: synchronise users from Microsoft 365.
+ *
+ * ## OPTIONS
+ *
+ * [--dry-run]
+ * : Only report what would change.
+ *
+ * ## EXAMPLES
+ *
+ * wp m365-login sync --dry-run
+ *
+ * @param array $args Positional arguments.
+ * @param array $assoc_args Flags.
+ */
+ public function cli( $args, $assoc_args ) {
+ $report = $this->run( ! empty( $assoc_args['dry-run'] ), 'cli' );
+ foreach ( $report['log'] as $entry ) {
+ WP_CLI::log( sprintf( '[%s] %s', $entry['level'], $entry['message'] ) );
+ }
+ foreach ( $report['counts'] as $key => $count ) {
+ WP_CLI::log( sprintf( '%s: %d', $key, $count ) );
+ }
+ if ( 'ok' === $report['status'] ) {
+ WP_CLI::success( $report['dry'] ? 'Dry run finished.' : 'Sync finished.' );
+ } else {
+ WP_CLI::error( 'Sync failed or was aborted.' );
+ }
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Run */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Last stored report or null.
+ *
+ * @return array|null
+ */
+ public static function last_report() {
+ $report = get_option( self::REPORT_OPTION, null );
+ return is_array( $report ) ? $report : null;
+ }
+
+ /**
+ * Runs a full sync.
+ *
+ * @param bool $dry Only simulate.
+ * @param string $trigger 'manual', 'cron' or 'cli'.
+ * @return array Report.
+ */
+ public function run( $dry = false, $trigger = 'manual' ) {
+ $this->dry = (bool) $dry;
+ $this->report = array(
+ 'started' => time(),
+ 'finished' => 0,
+ 'dry' => $this->dry,
+ 'trigger' => $trigger,
+ 'status' => 'ok',
+ 'counts' => array_fill_keys( array( 'created', 'updated', 'linked', 'unchanged', 'disabled', 'enabled', 'deleted', 'photos', 'skipped', 'errors' ), 0 ),
+ 'log' => array(),
+ );
+
+ if ( get_transient( self::LOCK ) ) {
+ $this->log( 'error', __( 'Another sync is still running. Please try again in a few minutes.', 'm365-login' ) );
+ return $this->finish( 'locked', false );
+ }
+ set_transient( self::LOCK, time(), self::LOCK_TTL );
+
+ if ( function_exists( 'set_time_limit' ) ) {
+ set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- long-running directory sync.
+ }
+ wp_raise_memory_limit( 'admin' );
+ require_once ABSPATH . 'wp-admin/includes/user.php';
+
+ // No "your e-mail/password changed" mails for changes made by the sync.
+ add_filter( 'send_email_change_email', '__return_false', 99 );
+ add_filter( 'send_password_change_email', '__return_false', 99 );
+
+ try {
+ $status = $this->sync();
+ } finally {
+ remove_filter( 'send_email_change_email', '__return_false', 99 );
+ remove_filter( 'send_password_change_email', '__return_false', 99 );
+ delete_transient( self::LOCK );
+ }
+
+ return $this->finish( $status, true );
+ }
+
+ /**
+ * Stores and returns the report.
+ *
+ * @param string $status 'ok', 'failed', 'aborted' or 'locked'.
+ * @param bool $store Whether to persist it.
+ * @return array
+ */
+ private function finish( $status, $store ) {
+ $this->report['status'] = $status;
+ $this->report['finished'] = time();
+ if ( $store ) {
+ update_option( self::REPORT_OPTION, $this->report, false );
+ }
+
+ /**
+ * Fires after a user sync run.
+ *
+ * @param array $report Report (counts, log, status, dry).
+ */
+ do_action( 'm365_login_sync_finished', $this->report );
+
+ return $this->report;
+ }
+
+ /**
+ * The actual sync.
+ *
+ * @return string Status.
+ */
+ private function sync() {
+ if ( ! $this->settings->is_configured() ) {
+ $this->log( 'error', __( 'The connection to Microsoft Entra ID is not configured yet.', 'm365-login' ) );
+ return 'failed';
+ }
+ if ( $this->settings->is_multi_tenant() ) {
+ $this->log( 'error', __( 'The user sync needs a pinned tenant ID (GUID) on the Connection tab.', 'm365-login' ) );
+ return 'failed';
+ }
+ if ( ! get_role( (string) $this->settings->get( 'sync_default_role' ) ) ) {
+ $this->log( 'error', __( 'The default role does not exist. Please check the sync settings.', 'm365-login' ) );
+ return 'failed';
+ }
+
+ // 1. Read the directory. Any error aborts the run before anything is changed.
+ $select = $this->graph_select();
+ $people = $this->fetch_people( $select );
+ if ( is_wp_error( $people ) ) {
+ $this->log( 'error', $this->graph_error_text( $people ) );
+ return 'failed';
+ }
+ /* translators: %d: number of users */
+ $this->log( 'info', sprintf( _n( '%d user read from Microsoft 365.', '%d users read from Microsoft 365.', count( $people ), 'm365-login' ), count( $people ) ) );
+
+ $memberships = $this->fetch_role_groups();
+ if ( is_wp_error( $memberships ) ) {
+ $this->log( 'error', $this->graph_error_text( $memberships ) );
+ return 'failed';
+ }
+
+ // 2. Create, link and update accounts.
+ $linked = $this->linked_users();
+ $seen = array();
+ $pending = array(); // Deactivations/deletions, applied after the safety check.
+ $photos = 0;
+ $photo_on = array_key_exists( 'photo', $this->selected_attributes() );
+
+ foreach ( $people as $person ) {
+ $oid = strtolower( (string) $person['id'] );
+ $seen[ $oid ] = true;
+
+ $result = $this->sync_person( $person, $linked, $memberships );
+ if ( is_array( $result ) ) {
+ $pending[] = $result;
+ } elseif ( $result instanceof WP_User && $photo_on && ! $this->dry ) {
+ $photos += $this->maybe_sync_photo( $result, $oid, $photos );
+ }
+ }
+
+ // 3. Linked accounts that were not part of the directory listing.
+ foreach ( $linked as $oid => $user_id ) {
+ if ( isset( $seen[ $oid ] ) ) {
+ continue;
+ }
+ $action = $this->classify_missing( $oid, $user_id );
+ if ( is_wp_error( $action ) ) {
+ $this->log( 'error', $this->graph_error_text( $action ) );
+ return 'failed';
+ }
+ if ( null !== $action ) {
+ $pending[] = $action;
+ }
+ }
+
+ // 4. Safety net: never deactivate or delete a large part of the linked accounts in one go.
+ $pending = array_values( array_filter( $pending, array( $this, 'is_effective_action' ) ) );
+ $limit = (int) apply_filters( 'm365_login_sync_deprovision_limit', max( 5, (int) ceil( count( $linked ) * 0.2 ) ), count( $linked ) );
+ if ( count( $pending ) > $limit ) {
+ $this->log(
+ 'error',
+ sprintf(
+ /* translators: 1: number of accounts, 2: limit */
+ __( 'Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter).', 'm365-login' ),
+ count( $pending ),
+ $limit
+ )
+ );
+ return 'aborted';
+ }
+ foreach ( $pending as $action ) {
+ $this->apply_action( $action );
+ }
+
+ return 'ok';
+ }
+
+ /**
+ * Graph properties to read for every user.
+ *
+ * @return string[]
+ */
+ private function graph_select() {
+ $select = array( 'id', 'accountEnabled', 'mail', 'userPrincipalName', 'userType', 'displayName' );
+ foreach ( array_keys( $this->selected_attributes() ) as $key ) {
+ if ( 'photo' !== $key && preg_match( '/^[A-Za-z]+$/', $key ) ) {
+ $select[] = $key;
+ }
+ }
+ return array_values( array_unique( $select ) );
+ }
+
+ /**
+ * Users in scope: the whole tenant or the (nested) members of the sync groups.
+ *
+ * @param string[] $select Properties.
+ * @return array[]|WP_Error
+ */
+ private function fetch_people( $select ) {
+ $groups = $this->settings->sync_scope_groups();
+ if ( empty( $groups ) ) {
+ return $this->graph->list_users( $select );
+ }
+ $people = array();
+ foreach ( $groups as $group_id => $name ) {
+ $members = $this->graph->list_group_users( $group_id, $select );
+ if ( is_wp_error( $members ) ) {
+ return $members;
+ }
+ foreach ( $members as $member ) {
+ $people[ strtolower( (string) $member['id'] ) ] = $member;
+ }
+ }
+ return array_values( $people );
+ }
+
+ /**
+ * Members (object IDs) of every group used in the role mapping.
+ *
+ * @return array|WP_Error group ID => array( oid => true ).
+ */
+ private function fetch_role_groups() {
+ $out = array();
+ foreach ( array_keys( $this->settings->sync_role_map() ) as $group_id ) {
+ $members = $this->graph->list_group_users( $group_id, array( 'id' ) );
+ if ( is_wp_error( $members ) ) {
+ return $members;
+ }
+ $out[ $group_id ] = array();
+ foreach ( $members as $member ) {
+ $out[ $group_id ][ strtolower( (string) $member['id'] ) ] = true;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * WordPress users linked to a Microsoft object ID (this site only).
+ *
+ * @return array oid => user ID.
+ */
+ private function linked_users() {
+ $users = get_users(
+ array(
+ 'meta_key' => M365_Login_Auth::META_OID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_compare' => 'EXISTS',
+ 'fields' => array( 'ID' ),
+ 'number' => -1,
+ )
+ );
+ $out = array();
+ foreach ( $users as $row ) {
+ $oid = strtolower( (string) get_user_meta( (int) $row->ID, M365_Login_Auth::META_OID, true ) );
+ if ( M365_Login_Settings::is_guid( $oid ) ) {
+ $out[ $oid ] = (int) $row->ID;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Creates, links or updates the account of one directory user.
+ *
+ * @param array $person Graph user.
+ * @param array $linked oid => user ID (updated when an account is linked or created).
+ * @param array $memberships Role group memberships.
+ * @return WP_User|array|null The synced account, a pending deprovision action, or null when skipped.
+ */
+ private function sync_person( $person, &$linked, $memberships ) {
+ $oid = strtolower( (string) $person['id'] );
+ $upn = isset( $person['userPrincipalName'] ) ? (string) $person['userPrincipalName'] : $oid;
+ $enabled = ! isset( $person['accountEnabled'] ) || false !== $person['accountEnabled'];
+
+ if ( ! M365_Login_Settings::is_guid( $oid ) ) {
+ return null;
+ }
+
+ $user = isset( $linked[ $oid ] ) ? get_userdata( $linked[ $oid ] ) : false;
+
+ if ( ! $user && isset( $person['userType'] ) && 'Guest' === $person['userType'] && ! $this->settings->get( 'sync_guests' ) ) {
+ return null; // Guests are not imported (they may still be linked through a sign-in).
+ }
+
+ $email = $this->email_of( $person );
+ if ( '' === $email ) {
+ if ( ! $user ) {
+ /* translators: %s: user principal name */
+ $this->skip( sprintf( __( '%s: no usable e-mail address, skipped.', 'm365-login' ), $upn ) );
+ }
+ return $user ? $user : null;
+ }
+ if ( ! $this->domain_allowed( $email ) ) {
+ if ( ! $user ) {
+ /* translators: %s: e-mail address */
+ $this->skip( sprintf( __( '%s: e-mail domain is not on the allow-list, skipped.', 'm365-login' ), $email ) );
+ }
+ return null;
+ }
+
+ // Not linked yet: match an existing account by e-mail address.
+ if ( ! $user ) {
+ $by_mail = get_user_by( 'email', $email );
+ if ( $by_mail instanceof WP_User ) {
+ $stored = strtolower( (string) get_user_meta( $by_mail->ID, M365_Login_Auth::META_OID, true ) );
+ if ( '' !== $stored && $stored !== $oid ) {
+ /* translators: %s: e-mail address */
+ $this->skip( sprintf( __( '%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped.', 'm365-login' ), $email ) );
+ return null;
+ }
+ $user = $by_mail;
+ $this->log( 'info', sprintf( /* translators: %s: e-mail address */ __( '%s: existing account linked.', 'm365-login' ), $email ) );
+ $this->count( 'linked' );
+ if ( ! $this->dry ) {
+ update_user_meta( $user->ID, M365_Login_Auth::META_OID, $oid );
+ }
+ $linked[ $oid ] = $user->ID;
+ }
+ }
+
+ // Disabled in Microsoft 365.
+ if ( ! $enabled ) {
+ if ( ! $user ) {
+ return null; // Nothing to create for disabled people.
+ }
+ return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ) );
+ }
+
+ if ( ! $user ) {
+ return $this->create_user( $person, $oid, $email, $linked, $memberships );
+ }
+
+ if ( is_multisite() && ! is_user_member_of_blog( $user->ID ) ) {
+ if ( ! $this->dry ) {
+ add_user_to_blog( get_current_blog_id(), $user->ID, (string) $this->settings->get( 'sync_default_role' ) );
+ }
+ $this->log( 'info', sprintf( /* translators: %s: e-mail address */ __( '%s: added to this site.', 'm365-login' ), $email ) );
+ }
+
+ // Accounts deactivated by the sync come back when the person is active again.
+ $disabled = self::disabled_info( $user->ID );
+ if ( $disabled && 'sync' === $disabled['by'] ) {
+ if ( ! $this->dry ) {
+ self::enable( $user->ID );
+ }
+ $this->log( 'info', sprintf( /* translators: %s: e-mail address */ __( '%s: reactivated (active in Microsoft 365 again).', 'm365-login' ), $email ) );
+ $this->count( 'enabled' );
+ }
+
+ $changes = $this->update_profile( $user, $person, $email );
+ if ( $this->manages_roles( $user ) ) {
+ $changes = array_merge( $changes, $this->update_roles( $user, $this->desired_roles( $oid, $memberships ) ) );
+ }
+
+ if ( $changes ) {
+ /* translators: 1: e-mail address, 2: list of changed fields */
+ $this->log( 'info', sprintf( __( '%1$s: updated (%2$s).', 'm365-login' ), $email, implode( ', ', $changes ) ) );
+ $this->count( 'updated' );
+ } else {
+ $this->count( 'unchanged' );
+ }
+ if ( ! $this->dry ) {
+ update_user_meta( $user->ID, self::META_LAST_SYNC, time() );
+ }
+
+ return $user;
+ }
+
+ /**
+ * Creates a new WordPress account for a directory user.
+ *
+ * @param array $person Graph user.
+ * @param string $oid Object ID.
+ * @param string $email E-mail address.
+ * @param array $linked oid => user ID.
+ * @param array $memberships Role group memberships.
+ * @return WP_User|null
+ */
+ private function create_user( $person, $oid, $email, &$linked, $memberships ) {
+ $roles = $this->desired_roles( $oid, $memberships );
+
+ /* translators: 1: e-mail address, 2: role names */
+ $this->log( 'info', sprintf( __( '%1$s: account created (%2$s).', 'm365-login' ), $email, $this->role_names( $roles ) ) );
+ $this->count( 'created' );
+ if ( $this->dry ) {
+ return null;
+ }
+
+ $data = array(
+ 'user_login' => $this->unique_login( $email ),
+ 'user_email' => $email,
+ 'user_pass' => wp_generate_password( 40, true, true ),
+ 'role' => $roles[0],
+ 'display_name' => ! empty( $person['displayName'] ) ? sanitize_text_field( (string) $person['displayName'] ) : $email,
+ );
+
+ /**
+ * Filters the data used to create a WordPress account for a Microsoft 365 user.
+ *
+ * @param array $data Arguments for wp_insert_user().
+ * @param array $person Graph user object.
+ */
+ $data = apply_filters( 'm365_login_sync_new_user_data', $data, $person );
+
+ $user_id = wp_insert_user( $data );
+ if ( is_wp_error( $user_id ) ) {
+ $this->count( 'created', -1 );
+ $this->count( 'errors' );
+ /* translators: 1: e-mail address, 2: error message */
+ $this->log( 'error', sprintf( __( '%1$s: account could not be created: %2$s', 'm365-login' ), $email, $user_id->get_error_message() ) );
+ return null;
+ }
+
+ update_user_meta( $user_id, M365_Login_Auth::META_OID, $oid );
+ update_user_meta( $user_id, self::META_SYNCED, time() );
+ update_user_meta( $user_id, self::META_LAST_SYNC, time() );
+ $linked[ $oid ] = (int) $user_id;
+
+ $user = get_userdata( $user_id );
+ $this->update_profile( $user, $person, $email );
+ $this->update_roles( $user, $roles );
+
+ /**
+ * Fires after the sync created a WordPress account.
+ *
+ * @param WP_User $user New user.
+ * @param array $person Graph user object.
+ */
+ do_action( 'm365_login_sync_user_created', $user, $person );
+
+ return $user;
+ }
+
+ /**
+ * Unique user_login derived from the e-mail address.
+ *
+ * @param string $email E-mail address.
+ * @return string
+ */
+ private function unique_login( $email ) {
+ $base = sanitize_user( strtok( $email, '@' ), true );
+ $base = '' === $base ? 'm365user' : mb_substr( $base, 0, 50 );
+ $login = $base;
+ $suffix = 2;
+ while ( username_exists( $login ) ) {
+ $login = $base . $suffix;
+ ++$suffix;
+ }
+ return $login;
+ }
+
+ /**
+ * Copies e-mail address and selected attributes into the profile.
+ *
+ * @param WP_User $user User.
+ * @param array $person Graph user.
+ * @param string $email E-mail from the directory.
+ * @return string[] Changed fields (for the log).
+ */
+ private function update_profile( $user, $person, $email ) {
+ $changes = array();
+ $fields = array();
+
+ if ( strtolower( $user->user_email ) !== $email ) {
+ $owner = get_user_by( 'email', $email );
+ if ( $owner && $owner->ID !== $user->ID ) {
+ /* translators: %s: e-mail address */
+ $this->log( 'warning', sprintf( __( '%s: e-mail address is used by another WordPress account and was not changed.', 'm365-login' ), $email ) );
+ } else {
+ $fields['user_email'] = $email;
+ $changes[] = __( 'e-mail', 'm365-login' );
+ }
+ }
+
+ $labels = self::attributes();
+ foreach ( $this->selected_attributes() as $key => $target ) {
+ if ( 'avatar' === $target ) {
+ continue;
+ }
+ $value = $this->attribute_value( $person, $key, $target );
+ if ( null === $value ) {
+ continue;
+ }
+
+ if ( in_array( $target, array( 'display_name', 'first_name', 'last_name', 'locale' ), true ) ) {
+ if ( '' === $value && 'display_name' === $target ) {
+ continue;
+ }
+ if ( (string) $user->$target !== $value ) {
+ $fields[ $target ] = $value;
+ $changes[] = $labels[ $key ]['label'];
+ }
+ continue;
+ }
+
+ $meta_key = sanitize_key( $target );
+ if ( (string) get_user_meta( $user->ID, $meta_key, true ) !== $value ) {
+ if ( ! $this->dry ) {
+ if ( '' === $value ) {
+ delete_user_meta( $user->ID, $meta_key );
+ } else {
+ update_user_meta( $user->ID, $meta_key, $value );
+ }
+ }
+ $changes[] = $labels[ $key ]['label'];
+ }
+ }
+
+ if ( $fields && ! $this->dry ) {
+ $fields['ID'] = $user->ID;
+ $result = wp_update_user( $fields );
+ if ( is_wp_error( $result ) ) {
+ $this->count( 'errors' );
+ /* translators: 1: e-mail address, 2: error message */
+ $this->log( 'error', sprintf( __( '%1$s: profile could not be updated: %2$s', 'm365-login' ), $email, $result->get_error_message() ) );
+ return array();
+ }
+ clean_user_cache( $user->ID );
+ }
+
+ return $changes;
+ }
+
+ /**
+ * Normalised value of one attribute, or null to leave the field alone.
+ *
+ * @param array $person Graph user.
+ * @param string $key Graph property.
+ * @param string $target Target field.
+ * @return string|null
+ */
+ private function attribute_value( $person, $key, $target ) {
+ if ( ! array_key_exists( $key, $person ) ) {
+ return null;
+ }
+ $raw = $person[ $key ];
+ if ( is_array( $raw ) ) {
+ $raw = isset( $raw[0] ) && is_scalar( $raw[0] ) ? $raw[0] : '';
+ }
+ $value = null === $raw ? '' : sanitize_text_field( (string) $raw );
+
+ if ( 'locale' === $target ) {
+ $locale = str_replace( '-', '_', $value );
+ if ( '' === $locale ) {
+ return null;
+ }
+ if ( 'en_US' !== $locale && ! in_array( $locale, get_available_languages(), true ) ) {
+ return null; // Language pack not installed: keep the site default.
+ }
+ return $locale;
+ }
+ return mb_substr( $value, 0, 250 );
+ }
+
+ /**
+ * Roles a person should have, in order (the first one is the primary role).
+ *
+ * @param string $oid Object ID.
+ * @param array $memberships Role group memberships.
+ * @return string[]
+ */
+ private function desired_roles( $oid, $memberships ) {
+ $default = (string) $this->settings->get( 'sync_default_role' );
+ $mapped = array();
+ foreach ( $this->settings->sync_role_map() as $group_id => $row ) {
+ if ( isset( $memberships[ $group_id ][ $oid ] ) && get_role( $row['role'] ) ) {
+ $mapped[] = $row['role'];
+ }
+ }
+ $mapped = array_values( array_unique( $mapped ) );
+
+ if ( 'replace' === $this->settings->get( 'sync_role_mode' ) ) {
+ $roles = $mapped ? array( $mapped[0] ) : array( $default );
+ } else {
+ $roles = array_values( array_unique( array_merge( array( $default ), $mapped ) ) );
+ }
+
+ /**
+ * Filters the WordPress roles the sync assigns to a Microsoft 365 user.
+ *
+ * @param string[] $roles Role slugs, the first is the primary role.
+ * @param string $oid Microsoft object ID.
+ */
+ $roles = array_values( array_filter( (array) apply_filters( 'm365_login_sync_roles', $roles, $oid ), 'get_role' ) );
+ return $roles ? $roles : array( $default );
+ }
+
+ /**
+ * Whether the sync may change the roles of this account.
+ *
+ * @param WP_User $user User.
+ * @return bool
+ */
+ private function manages_roles( $user ) {
+ if ( $this->is_protected( $user ) ) {
+ return false;
+ }
+ return (bool) get_user_meta( $user->ID, self::META_SYNCED, true ) || (bool) $this->settings->get( 'sync_manage_existing' );
+ }
+
+ /**
+ * Applies the desired roles.
+ *
+ * @param WP_User $user User.
+ * @param string[] $roles Desired roles.
+ * @return string[] Changes for the log.
+ */
+ private function update_roles( $user, $roles ) {
+ $current = array_values( $user->roles );
+ $same = count( $current ) === count( $roles ) && ! array_diff( $current, $roles ) && reset( $current ) === $roles[0];
+ if ( $same ) {
+ return array();
+ }
+ if ( ! $this->dry ) {
+ $user->set_role( $roles[0] );
+ foreach ( array_slice( $roles, 1 ) as $role ) {
+ $user->add_role( $role );
+ }
+ }
+ /* translators: %s: role names */
+ return array( sprintf( __( 'roles: %s', 'm365-login' ), $this->role_names( $roles ) ) );
+ }
+
+ /**
+ * Human readable role list.
+ *
+ * @param string[] $roles Role slugs.
+ * @return string
+ */
+ private function role_names( $roles ) {
+ $names = wp_roles()->get_names();
+ $out = array();
+ foreach ( $roles as $role ) {
+ $out[] = isset( $names[ $role ] ) ? translate_user_role( $names[ $role ] ) : $role;
+ }
+ return implode( ', ', $out );
+ }
+
+ /**
+ * Decides what happens to a linked account that was not in the directory listing.
+ *
+ * @param string $oid Object ID.
+ * @param int $user_id User ID.
+ * @return array|null|WP_Error Pending action, null for none.
+ */
+ private function classify_missing( $oid, $user_id ) {
+ $user = get_userdata( $user_id );
+ if ( ! $user ) {
+ return null;
+ }
+
+ // Double-check with Graph: only a 404 proves that the account was deleted.
+ $person = $this->graph->get_user( $oid, array( 'id', 'accountEnabled', 'userType' ) );
+ if ( is_wp_error( $person ) ) {
+ if ( M365_Login_Graph::is_not_found( $person ) ) {
+ return $this->action( (string) $this->settings->get( 'sync_deleted_action' ), $user, 'deleted', __( 'deleted in Microsoft 365', 'm365-login' ) );
+ }
+ return $person;
+ }
+ if ( isset( $person['accountEnabled'] ) && false === $person['accountEnabled'] ) {
+ return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ) );
+ }
+ if ( $this->settings->sync_scope_groups() && ( ! isset( $person['userType'] ) || 'Guest' !== $person['userType'] || $this->settings->get( 'sync_guests' ) ) ) {
+ return $this->action( (string) $this->settings->get( 'sync_scope_action' ), $user, 'scope', __( 'no longer a member of the sync groups', 'm365-login' ) );
+ }
+ return null;
+ }
+
+ /**
+ * Builds a pending deprovision action.
+ *
+ * @param string $what 'none', 'disable' or 'delete'.
+ * @param WP_User $user User.
+ * @param string $reason Machine reason.
+ * @param string $label Human reason.
+ * @return array|null
+ */
+ private function action( $what, $user, $reason, $label ) {
+ if ( ! in_array( $what, array( 'disable', 'delete' ), true ) ) {
+ return null;
+ }
+ if ( $this->is_protected( $user ) ) {
+ /* translators: 1: e-mail address, 2: reason */
+ $this->skip( sprintf( __( '%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed.', 'm365-login' ), $user->user_email, $label ) );
+ return null;
+ }
+ return array(
+ 'what' => $what,
+ 'user' => $user,
+ 'reason' => $reason,
+ 'label' => $label,
+ );
+ }
+
+ /**
+ * Filters out actions that would not change anything (already deactivated).
+ *
+ * @param array $action Pending action.
+ * @return bool
+ */
+ private function is_effective_action( $action ) {
+ return 'delete' === $action['what'] || ! self::disabled_info( $action['user']->ID );
+ }
+
+ /**
+ * Deactivates or deletes an account.
+ *
+ * @param array $action Pending action.
+ */
+ private function apply_action( $action ) {
+ $user = $action['user'];
+ $reassign = (int) $this->settings->get( 'sync_reassign' );
+ $what = $action['what'];
+
+ if ( 'delete' === $what && ( ! $reassign || $reassign === $user->ID || ! get_userdata( $reassign ) ) ) {
+ /* translators: %s: e-mail address */
+ $this->log( 'warning', sprintf( __( '%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted.', 'm365-login' ), $user->user_email ) );
+ $what = 'disable';
+ if ( self::disabled_info( $user->ID ) ) {
+ return;
+ }
+ }
+
+ if ( 'delete' === $what ) {
+ /* translators: 1: e-mail address, 2: reason */
+ $this->log( 'info', sprintf( __( '%1$s: account deleted (%2$s).', 'm365-login' ), $user->user_email, $action['label'] ) );
+ $this->count( 'deleted' );
+ if ( ! $this->dry ) {
+ $this->delete_photo( $user->ID );
+ wp_delete_user( $user->ID, $reassign );
+ }
+ return;
+ }
+
+ /* translators: 1: e-mail address, 2: reason */
+ $this->log( 'info', sprintf( __( '%1$s: account deactivated (%2$s).', 'm365-login' ), $user->user_email, $action['label'] ) );
+ $this->count( 'disabled' );
+ if ( ! $this->dry ) {
+ self::disable( $user->ID, 'sync', $action['reason'] );
+ }
+ }
+
+ /**
+ * Accounts the sync never deactivates, deletes or re-roles.
+ *
+ * Administrators that existed before the sync are protected; accounts the sync
+ * created (and may have promoted through a group mapping) are fully managed.
+ *
+ * @param WP_User $user User.
+ * @return bool
+ */
+ private function is_protected( $user ) {
+ $protected = get_current_user_id() === $user->ID
+ || ( is_multisite() && is_super_admin( $user->ID ) )
+ || ( ! get_user_meta( $user->ID, self::META_SYNCED, true ) && user_can( $user, 'manage_options' ) );
+
+ /**
+ * Filters whether the sync must leave an account alone (no role changes, deactivation or deletion).
+ *
+ * @param bool $protected Whether the account is protected.
+ * @param WP_User $user User.
+ */
+ return (bool) apply_filters( 'm365_login_sync_protect_user', $protected, $user );
+ }
+
+ /**
+ * E-mail address of a directory user (mail, else a usable UPN).
+ *
+ * @param array $person Graph user.
+ * @return string Lowercase address or ''.
+ */
+ private function email_of( $person ) {
+ $candidates = array();
+ if ( ! empty( $person['mail'] ) ) {
+ $candidates[] = (string) $person['mail'];
+ }
+ if ( ! empty( $person['userPrincipalName'] ) && false === stripos( (string) $person['userPrincipalName'], '#ext#' ) ) {
+ $candidates[] = (string) $person['userPrincipalName'];
+ }
+ foreach ( $candidates as $candidate ) {
+ $candidate = strtolower( trim( $candidate ) );
+ if ( is_email( $candidate ) ) {
+ /**
+ * Filters the e-mail address the sync uses for a Microsoft 365 user.
+ *
+ * @param string $email Address.
+ * @param array $person Graph user object.
+ */
+ return strtolower( (string) apply_filters( 'm365_login_sync_email', $candidate, $person ) );
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Domain allow-list from the Security tab.
+ *
+ * @param string $email E-mail.
+ * @return bool
+ */
+ private function domain_allowed( $email ) {
+ $allowed = $this->settings->allowed_domains();
+ return empty( $allowed ) || in_array( strtolower( substr( strrchr( $email, '@' ), 1 ) ), $allowed, true );
+ }
+
+ /**
+ * Friendlier text for common Graph permission errors.
+ *
+ * @param WP_Error $error Error.
+ * @return string
+ */
+ private function graph_error_text( $error ) {
+ $message = $error->get_error_message();
+ if ( false !== stripos( $message, 'Authorization_RequestDenied' ) || false !== stripos( $message, 'Insufficient privileges' ) ) {
+ return __( 'Microsoft Graph refused the request. Grant the application permissions "User.Read.All" and "GroupMember.Read.All" with admin consent in Entra ID.', 'm365-login' );
+ }
+ /* translators: %s: error message */
+ return sprintf( __( 'Microsoft Graph error: %s', 'm365-login' ), $message );
+ }
+
+ /**
+ * Adds a log line.
+ *
+ * @param string $level 'info', 'warning' or 'error'.
+ * @param string $message Message.
+ */
+ private function log( $level, $message ) {
+ if ( count( $this->report['log'] ) < self::LOG_LIMIT ) {
+ $this->report['log'][] = array(
+ 'level' => $level,
+ 'message' => $message,
+ );
+ } elseif ( count( $this->report['log'] ) === self::LOG_LIMIT ) {
+ $this->report['log'][] = array(
+ 'level' => 'warning',
+ 'message' => __( 'Log truncated.', 'm365-login' ),
+ );
+ }
+ }
+
+ /**
+ * Logs a skipped person.
+ *
+ * @param string $message Message.
+ */
+ private function skip( $message ) {
+ $this->log( 'warning', $message );
+ $this->count( 'skipped' );
+ }
+
+ /**
+ * Increments a counter.
+ *
+ * @param string $key Counter.
+ * @param int $delta Amount.
+ */
+ private function count( $key, $delta = 1 ) {
+ $this->report['counts'][ $key ] += $delta;
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Profile photos */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Refreshes the profile photo when it was not checked recently.
+ *
+ * @param WP_User $user User.
+ * @param string $oid Object ID.
+ * @param int $done_so_far Photo checks done in this run.
+ * @return int 1 when Graph was asked, 0 otherwise.
+ */
+ private function maybe_sync_photo( $user, $oid, $done_so_far ) {
+ /**
+ * Maximum number of profile photo checks per sync run (the rest follows in later runs).
+ *
+ * @param int $limit Limit.
+ */
+ if ( $done_so_far >= (int) apply_filters( 'm365_login_sync_photo_limit', 200 ) ) {
+ return 0;
+ }
+ $stored = get_user_meta( $user->ID, self::META_PHOTO, true );
+ $stored = is_array( $stored ) ? $stored : array();
+
+ /**
+ * Seconds between two photo checks of the same user.
+ *
+ * @param int $interval Interval.
+ */
+ $interval = (int) apply_filters( 'm365_login_sync_photo_interval', 20 * HOUR_IN_SECONDS );
+ if ( ! empty( $stored['checked'] ) && time() - (int) $stored['checked'] < $interval ) {
+ return 0;
+ }
+
+ $info = $this->graph->photo_info( $oid );
+ if ( is_wp_error( $info ) ) {
+ /* translators: 1: e-mail address, 2: error message */
+ $this->log( 'warning', sprintf( __( '%1$s: profile photo could not be read: %2$s', 'm365-login' ), $user->user_email, $info->get_error_message() ) );
+ return 1;
+ }
+
+ if ( null === $info ) {
+ if ( ! empty( $stored['file'] ) ) {
+ $this->delete_photo( $user->ID );
+ /* translators: %s: e-mail address */
+ $this->log( 'info', sprintf( __( '%s: profile photo removed.', 'm365-login' ), $user->user_email ) );
+ $this->count( 'photos' );
+ }
+ update_user_meta( $user->ID, self::META_PHOTO, array( 'checked' => time() ) );
+ return 1;
+ }
+
+ if ( ! empty( $stored['file'] ) && isset( $stored['etag'] ) && $stored['etag'] === $info['etag'] && file_exists( self::photo_path( $stored['file'] ) ) ) {
+ $stored['checked'] = time();
+ update_user_meta( $user->ID, self::META_PHOTO, $stored );
+ return 1;
+ }
+
+ $bytes = $this->graph->photo_bytes( $info['path'] );
+ if ( is_wp_error( $bytes ) || '' === $bytes || strlen( $bytes ) > self::PHOTO_MAX ) {
+ /* translators: %s: e-mail address */
+ $this->log( 'warning', sprintf( __( '%s: profile photo could not be downloaded.', 'm365-login' ), $user->user_email ) );
+ return 1;
+ }
+
+ $file = $this->store_photo( $user->ID, $oid, $info['etag'], $bytes );
+ if ( '' === $file ) {
+ /* translators: %s: e-mail address */
+ $this->log( 'warning', sprintf( __( '%s: profile photo is not a valid image or could not be saved.', 'm365-login' ), $user->user_email ) );
+ return 1;
+ }
+
+ if ( ! empty( $stored['file'] ) && $stored['file'] !== $file ) {
+ wp_delete_file( self::photo_path( $stored['file'] ) );
+ }
+ update_user_meta(
+ $user->ID,
+ self::META_PHOTO,
+ array(
+ 'file' => $file,
+ 'etag' => $info['etag'],
+ 'checked' => time(),
+ )
+ );
+ /* translators: %s: e-mail address */
+ $this->log( 'info', sprintf( __( '%s: profile photo updated.', 'm365-login' ), $user->user_email ) );
+ $this->count( 'photos' );
+ return 1;
+ }
+
+ /**
+ * Writes the image into uploads/m365-login-avatars/.
+ *
+ * @param int $user_id User ID.
+ * @param string $oid Object ID.
+ * @param string $etag Photo version.
+ * @param string $bytes Image data.
+ * @return string File path relative to the uploads base directory, or ''.
+ */
+ private function store_photo( $user_id, $oid, $etag, $bytes ) {
+ $size = @getimagesizefromstring( $bytes ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- invalid data is expected to fail quietly.
+ $exts = array(
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/gif' => 'gif',
+ );
+ if ( ! is_array( $size ) || empty( $size['mime'] ) || ! isset( $exts[ $size['mime'] ] ) ) {
+ return '';
+ }
+
+ $name = 'm365-' . substr( wp_hash( $oid . '|avatar' ), 0, 16 ) . '-' . substr( md5( $etag ), 0, 8 ) . '.' . $exts[ $size['mime'] ];
+ $subdir = static function ( $dirs ) {
+ $dirs['subdir'] = '/' . self::PHOTO_DIR;
+ $dirs['path'] = $dirs['basedir'] . $dirs['subdir'];
+ $dirs['url'] = $dirs['baseurl'] . $dirs['subdir'];
+ return $dirs;
+ };
+
+ add_filter( 'upload_dir', $subdir );
+ $existing = wp_upload_dir();
+ if ( file_exists( trailingslashit( $existing['path'] ) . $name ) ) {
+ wp_delete_file( trailingslashit( $existing['path'] ) . $name );
+ }
+ $upload = wp_upload_bits( $name, null, $bytes );
+ remove_filter( 'upload_dir', $subdir );
+
+ if ( ! empty( $upload['error'] ) || empty( $upload['file'] ) ) {
+ return '';
+ }
+ $uploads = wp_get_upload_dir();
+ return ltrim( str_replace( wp_normalize_path( $uploads['basedir'] ), '', wp_normalize_path( $upload['file'] ) ), '/' );
+ }
+
+ /**
+ * Absolute path of a stored photo.
+ *
+ * @param string $file Relative path.
+ * @return string
+ */
+ private static function photo_path( $file ) {
+ $uploads = wp_get_upload_dir();
+ return trailingslashit( $uploads['basedir'] ) . ltrim( $file, '/' );
+ }
+
+ /**
+ * Deletes a user's stored photo.
+ *
+ * @param int $user_id User ID.
+ */
+ private function delete_photo( $user_id ) {
+ $stored = get_user_meta( $user_id, self::META_PHOTO, true );
+ if ( is_array( $stored ) && ! empty( $stored['file'] ) && 0 === strpos( $stored['file'], self::PHOTO_DIR . '/' ) ) {
+ wp_delete_file( self::photo_path( $stored['file'] ) );
+ }
+ delete_user_meta( $user_id, self::META_PHOTO );
+ }
+
+ /**
+ * Uses the synced Microsoft 365 photo as avatar.
+ *
+ * @param array $args Avatar data.
+ * @param mixed $id_or_email User ID, e-mail, WP_User, WP_Post or WP_Comment.
+ * @return array
+ */
+ public function avatar_data( $args, $id_or_email ) {
+ if ( ! in_array( 'photo', (array) $this->settings->get( 'sync_attributes', array() ), true ) ) {
+ return $args;
+ }
+
+ $user_id = 0;
+ if ( is_numeric( $id_or_email ) ) {
+ $user_id = (int) $id_or_email;
+ } elseif ( $id_or_email instanceof WP_User ) {
+ $user_id = $id_or_email->ID;
+ } elseif ( $id_or_email instanceof WP_Post ) {
+ $user_id = (int) $id_or_email->post_author;
+ } elseif ( $id_or_email instanceof WP_Comment ) {
+ $user_id = (int) $id_or_email->user_id;
+ } elseif ( is_string( $id_or_email ) && is_email( $id_or_email ) ) {
+ $user = get_user_by( 'email', $id_or_email );
+ $user_id = $user ? $user->ID : 0;
+ }
+ if ( ! $user_id ) {
+ return $args;
+ }
+
+ $stored = get_user_meta( $user_id, self::META_PHOTO, true );
+ if ( ! is_array( $stored ) || empty( $stored['file'] ) ) {
+ return $args;
+ }
+ $uploads = wp_get_upload_dir();
+ $args['url'] = trailingslashit( $uploads['baseurl'] ) . ltrim( $stored['file'], '/' );
+ $args['found_avatar'] = true;
+ return $args;
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Deactivated accounts */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Deactivation details or null when the account is active.
+ *
+ * @param int $user_id User ID.
+ * @return array|null
+ */
+ public static function disabled_info( $user_id ) {
+ $info = get_user_meta( (int) $user_id, self::META_DISABLED, true );
+ if ( ! is_array( $info ) || empty( $info['time'] ) ) {
+ return null;
+ }
+ return wp_parse_args(
+ $info,
+ array(
+ 'by' => 'manual',
+ 'reason' => '',
+ )
+ );
+ }
+
+ /**
+ * Deactivates an account and ends all of its sessions.
+ *
+ * @param int $user_id User ID.
+ * @param string $by 'sync' or 'manual'.
+ * @param string $reason Machine reason.
+ */
+ public static function disable( $user_id, $by, $reason = '' ) {
+ update_user_meta(
+ $user_id,
+ self::META_DISABLED,
+ array(
+ 'time' => time(),
+ 'by' => $by,
+ 'reason' => $reason,
+ )
+ );
+ WP_Session_Tokens::get_instance( $user_id )->destroy_all();
+
+ /**
+ * Fires after an account was deactivated.
+ *
+ * @param int $user_id User ID.
+ * @param string $by 'sync' or 'manual'.
+ * @param string $reason 'disabled', 'deleted', 'scope' or ''.
+ */
+ do_action( 'm365_login_user_disabled', $user_id, $by, $reason );
+ }
+
+ /**
+ * Reactivates an account.
+ *
+ * @param int $user_id User ID.
+ */
+ public static function enable( $user_id ) {
+ delete_user_meta( $user_id, self::META_DISABLED );
+
+ /**
+ * Fires after an account was reactivated.
+ *
+ * @param int $user_id User ID.
+ */
+ do_action( 'm365_login_user_enabled', $user_id );
+ }
+
+ /**
+ * Refuses every sign-in (password, application password, Microsoft) of deactivated accounts.
+ *
+ * @param null|WP_User|WP_Error $user Result so far.
+ * @return null|WP_User|WP_Error
+ */
+ public function block_disabled_login( $user ) {
+ if ( $user instanceof WP_User && self::disabled_info( $user->ID ) ) {
+ return new WP_Error( 'm365_login_disabled', __( 'This account has been deactivated.', 'm365-login' ) );
+ }
+ return $user;
+ }
+
+ /**
+ * Treats existing sessions of deactivated accounts as logged out.
+ *
+ * @param int|false $user_id Detected user.
+ * @return int|false
+ */
+ public function drop_disabled_session( $user_id ) {
+ if ( $user_id && self::disabled_info( (int) $user_id ) ) {
+ return false;
+ }
+ return $user_id;
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Users screen and profile */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Adds the "Microsoft 365" column to the users list.
+ *
+ * @param string[] $columns Columns.
+ * @return string[]
+ */
+ public function users_column( $columns ) {
+ $columns['m365_login'] = __( 'Microsoft 365', 'm365-login' );
+ return $columns;
+ }
+
+ /**
+ * Renders the "Microsoft 365" column.
+ *
+ * @param string $output Output so far.
+ * @param string $column Column.
+ * @param int $user_id User ID.
+ * @return string
+ */
+ public function users_column_value( $output, $column, $user_id ) {
+ if ( 'm365_login' !== $column ) {
+ return $output;
+ }
+ $parts = array();
+ if ( self::disabled_info( $user_id ) ) {
+ $parts[] = '
' . esc_html__( 'Deactivated', 'm365-login' ) . '';
+ }
+ if ( get_user_meta( $user_id, self::META_SYNCED, true ) ) {
+ $parts[] = esc_html__( 'Imported', 'm365-login' );
+ } elseif ( get_user_meta( $user_id, M365_Login_Auth::META_OID, true ) ) {
+ $parts[] = esc_html__( 'Linked', 'm365-login' );
+ }
+ return $parts ? implode( '
', $parts ) : '—';
+ }
+
+ /**
+ * "Deactivate" / "Reactivate" row actions.
+ *
+ * @param string[] $actions Actions.
+ * @param WP_User $user User.
+ * @return string[]
+ */
+ public function user_row_actions( $actions, $user ) {
+ if ( ! current_user_can( 'edit_user', $user->ID ) || get_current_user_id() === $user->ID ) {
+ return $actions;
+ }
+ $disabled = (bool) self::disabled_info( $user->ID );
+ $url = wp_nonce_url(
+ add_query_arg(
+ array(
+ 'action' => self::POST_STATE,
+ 'user_id' => $user->ID,
+ 'state' => $disabled ? 'enable' : 'disable',
+ ),
+ admin_url( 'admin-post.php' )
+ ),
+ self::POST_STATE . '_' . $user->ID
+ );
+ $actions['m365_login_state'] = '
' . ( $disabled ? esc_html__( 'Reactivate', 'm365-login' ) : esc_html__( 'Deactivate', 'm365-login' ) ) . '';
+ return $actions;
+ }
+
+ /**
+ * Handles the row actions.
+ */
+ public function handle_user_state() {
+ $user_id = isset( $_GET['user_id'] ) ? absint( $_GET['user_id'] ) : 0;
+ check_admin_referer( self::POST_STATE . '_' . $user_id );
+ if ( ! $user_id || ! current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id ) {
+ wp_die( esc_html__( 'You are not allowed to do this.', 'm365-login' ), 403 );
+ }
+ $state = isset( $_GET['state'] ) ? sanitize_key( wp_unslash( $_GET['state'] ) ) : '';
+ if ( 'disable' === $state ) {
+ self::disable( $user_id, 'manual' );
+ } else {
+ self::enable( $user_id );
+ }
+ wp_safe_redirect( add_query_arg( 'm365_user_state', 'disable' === $state ? 'disabled' : 'enabled', admin_url( 'users.php' ) ) );
+ exit;
+ }
+
+ /**
+ * Confirmation after a row action.
+ */
+ public function user_state_notice() {
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only.
+ $state = isset( $_GET['m365_user_state'] ) ? sanitize_key( wp_unslash( $_GET['m365_user_state'] ) ) : '';
+ if ( '' === $state ) {
+ return;
+ }
+ $text = 'disabled' === $state
+ ? __( 'The account has been deactivated and signed out everywhere.', 'm365-login' )
+ : __( 'The account has been reactivated.', 'm365-login' );
+ printf( '
', esc_html( $text ) );
+ }
+
+ /**
+ * Read-only "Microsoft 365" section on the profile screen.
+ *
+ * @param WP_User $user User being edited.
+ */
+ public function profile_section( $user ) {
+ $oid = (string) get_user_meta( $user->ID, M365_Login_Auth::META_OID, true );
+ if ( '' === $oid && ! self::disabled_info( $user->ID ) ) {
+ return;
+ }
+ $rows = array();
+ $disabled = self::disabled_info( $user->ID );
+ if ( $disabled ) {
+ $reasons = array(
+ 'disabled' => __( 'disabled in Microsoft 365', 'm365-login' ),
+ 'deleted' => __( 'deleted in Microsoft 365', 'm365-login' ),
+ 'scope' => __( 'no longer a member of the sync groups', 'm365-login' ),
+ );
+ $text = sprintf(
+ /* translators: 1: date, 2: reason */
+ __( 'Deactivated since %1$s (%2$s)', 'm365-login' ),
+ wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $disabled['time'] ),
+ isset( $reasons[ $disabled['reason'] ] ) ? $reasons[ $disabled['reason'] ] : __( 'manually', 'm365-login' )
+ );
+ $rows[ __( 'Status', 'm365-login' ) ] = $text;
+ }
+ if ( '' !== $oid ) {
+ $rows[ __( 'Object ID', 'm365-login' ) ] = $oid;
+ }
+ $last = (int) get_user_meta( $user->ID, self::META_LAST_SYNC, true );
+ if ( $last ) {
+ $rows[ __( 'Last sync', 'm365-login' ) ] = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last );
+ }
+ foreach ( self::attributes() as $attribute ) {
+ $target = (string) $attribute['target'];
+ if ( 0 !== strpos( $target, 'm365_' ) ) {
+ continue;
+ }
+ $value = (string) get_user_meta( $user->ID, $target, true );
+ if ( '' !== $value ) {
+ $rows[ (string) $attribute['label'] ] = $value;
+ }
+ }
+ ?>
+
+
+
+ settings = new M365_Login_Settings();
$this->graph = new M365_Login_Graph( $this->settings );
$this->auth = new M365_Login_Auth( $this->settings, $this->graph );
+ $this->sync = new M365_Login_Sync( $this->settings, $this->graph );
$this->button = new M365_Login_Button( $this->settings );
if ( is_admin() ) {
- $this->admin = new M365_Login_Admin( $this->settings, $this->auth, $this->graph );
+ $this->admin = new M365_Login_Admin( $this->settings, $this->auth, $this->graph, $this->sync );
}
add_filter( 'plugin_action_links_' . plugin_basename( M365_LOGIN_FILE ), array( $this, 'action_links' ) );
diff --git a/languages/m365-login-de_DE.mo b/languages/m365-login-de_DE.mo
index 36e39e2..dab20df 100644
Binary files a/languages/m365-login-de_DE.mo and b/languages/m365-login-de_DE.mo differ
diff --git a/languages/m365-login-de_DE.po b/languages/m365-login-de_DE.po
index bdba9a9..0386eb1 100644
--- a/languages/m365-login-de_DE.po
+++ b/languages/m365-login-de_DE.po
@@ -2,13 +2,13 @@
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
-"Project-Id-Version: M365 Login 1.0.0\n"
+"Project-Id-Version: M365 Login 1.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/friloo/wp-m365-login/issues\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-09-22T00:00:00+00:00\n"
-"PO-Revision-Date: 2026-09-22 12:00+0000\n"
+"POT-Creation-Date: 2026-09-23T00:00:00+00:00\n"
+"PO-Revision-Date: 2026-09-23 12:00+0000\n"
"Last-Translator: friloo\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -16,709 +16,1034 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:81 includes/class-m365-login-admin.php:82 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:382
+#: includes/class-m365-login-admin.php:92 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:104 includes/class-m365-login-admin.php:725
msgid "M365 Login"
msgstr "M365 Login"
-#: includes/class-m365-login-admin.php:108
+#: includes/class-m365-login-admin.php:119
msgid "Connection"
msgstr "Verbindung"
-#: includes/class-m365-login-admin.php:109
+#: includes/class-m365-login-admin.php:120
msgid "Button"
msgstr "Button"
-#: includes/class-m365-login-admin.php:110
+#: includes/class-m365-login-admin.php:121
msgid "Security"
msgstr "Sicherheit"
-#: includes/class-m365-login-admin.php:185
+#: includes/class-m365-login-admin.php:122
+msgid "User sync"
+msgstr "Benutzer-Sync"
+
+#: includes/class-m365-login-admin.php:197
msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
-#: includes/class-m365-login-admin.php:187
+#: includes/class-m365-login-admin.php:199
msgid "Open the settings"
msgstr "Einstellungen öffnen"
-#: includes/class-m365-login-admin.php:217
+#: includes/class-m365-login-admin.php:230
msgid "Choose button icon"
msgstr "Button-Icon auswählen"
-#: includes/class-m365-login-admin.php:218
+#: includes/class-m365-login-admin.php:231
msgid "Use this icon"
msgstr "Dieses Icon verwenden"
-#: includes/class-m365-login-admin.php:219
+#: includes/class-m365-login-admin.php:232
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:220 includes/class-m365-login-admin.php:505 includes/class-m365-login-admin.php:783 includes/class-m365-login-admin.php:826
+#: includes/class-m365-login-admin.php:233 includes/class-m365-login-admin.php:848 includes/class-m365-login-admin.php:1102 includes/class-m365-login-admin.php:1147
msgid "Copy"
msgstr "Kopieren"
-#: includes/class-m365-login-admin.php:221
+#: includes/class-m365-login-admin.php:234
msgid "Testing…"
msgstr "Wird geprüft …"
-#: includes/class-m365-login-admin.php:222
+#: includes/class-m365-login-admin.php:235
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
-#: includes/class-m365-login-admin.php:223
+#: includes/class-m365-login-admin.php:236
msgid "No groups found."
msgstr "Keine Gruppen gefunden."
-#: includes/class-m365-login-admin.php:224
+#: includes/class-m365-login-admin.php:237
msgid "Searching…"
msgstr "Suche läuft …"
-#: includes/class-m365-login-admin.php:225
+#: includes/class-m365-login-admin.php:238
msgid "Add"
msgstr "Hinzufügen"
-#: includes/class-m365-login-admin.php:226 includes/class-m365-login-admin.php:757
+#: includes/class-m365-login-admin.php:239 includes/class-m365-login-admin.php:495
msgid "Remove"
msgstr "Entfernen"
-#: includes/class-m365-login-admin.php:227 includes/class-m365-login-admin.php:285 includes/class-m365-login-admin.php:742
+#: includes/class-m365-login-admin.php:240 includes/class-m365-login-admin.php:303 includes/class-m365-login-admin.php:468
msgid "Save the connection settings first, then search for groups."
msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
-#: includes/class-m365-login-admin.php:228
+#: includes/class-m365-login-admin.php:241
msgid "Generate a new fallback key on save? The old link stops working."
msgstr "Beim Speichern einen neuen Fallback-Schlüssel erzeugen? Der alte Link funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:229
+#: includes/class-m365-login-admin.php:242
msgid "Generating a 3072-bit key pair, this takes a moment…"
msgstr "3072-Bit-Schlüsselpaar wird erzeugt, das dauert einen Moment …"
-#: includes/class-m365-login-admin.php:230
+#: includes/class-m365-login-admin.php:243
msgid "Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID."
msgstr "Gespeichertes Zertifikat ersetzen? Die Anmeldung funktioniert erst wieder, wenn das neue Zertifikat in Entra ID hochgeladen ist."
-#: includes/class-m365-login-admin.php:231
+#: includes/class-m365-login-admin.php:244
msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working."
msgstr "Gespeichertes Zertifikat beim Speichern entfernen? Die Anmeldung per Zertifikat funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:243 includes/class-m365-login-admin.php:282 includes/class-m365-login-admin.php:308 includes/class-m365-login-admin.php:340
+#: includes/class-m365-login-admin.php:245
+msgid "Sync is running, this can take a while for large directories…"
+msgstr "Sync läuft, bei großen Verzeichnissen kann das etwas dauern …"
+
+#: includes/class-m365-login-admin.php:246
+msgid "Run the sync now with the saved settings? Accounts are created, updated and possibly deactivated or deleted. Tip: run a dry run first."
+msgstr "Sync jetzt mit den gespeicherten Einstellungen ausführen? Konten werden angelegt, aktualisiert und eventuell deaktiviert oder gelöscht. Tipp: Führe zuerst einen Testlauf aus."
+
+#: includes/class-m365-login-admin.php:247
+msgid "The request failed or timed out. Reload the page in a few minutes to see the report; for very large directories use \"wp m365-login sync\" (WP-CLI)."
+msgstr "Die Anfrage ist fehlgeschlagen oder hat zu lange gedauert. Lade die Seite in ein paar Minuten neu, um den Bericht zu sehen; für sehr große Verzeichnisse nutze „wp m365-login sync“ (WP-CLI)."
+
+#: includes/class-m365-login-admin.php:248
+msgid "You have unsaved changes. The sync uses the saved settings – save first."
+msgstr "Du hast ungespeicherte Änderungen. Der Sync verwendet die gespeicherten Einstellungen – speichere zuerst."
+
+#: includes/class-m365-login-admin.php:249 includes/class-m365-login-admin.php:482
+msgid "Move up"
+msgstr "Nach oben"
+
+#: includes/class-m365-login-admin.php:261 includes/class-m365-login-admin.php:300 includes/class-m365-login-admin.php:326 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:514 includes/class-m365-login-sync.php:1495
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:248
+#: includes/class-m365-login-admin.php:266
msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:262
+#: includes/class-m365-login-admin.php:280
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:271
+#: includes/class-m365-login-admin.php:289
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
-#: includes/class-m365-login-admin.php:294
+#: includes/class-m365-login-admin.php:312
msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr "Microsoft Graph hat die Anfrage abgelehnt. In Entra ID die Anwendungsberechtigung „GroupMember.Read.All“ (oder „Directory.Read.All“) mit Administratorzustimmung erteilen."
-#: includes/class-m365-login-admin.php:312
+#: includes/class-m365-login-admin.php:330
msgid "Unknown operation."
msgstr "Unbekannte Aktion."
-#: includes/class-m365-login-admin.php:329
+#: includes/class-m365-login-admin.php:347
msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID."
msgstr "Zertifikat erzeugt und gespeichert. Jetzt die .cer-Datei herunterladen und in Entra ID hochladen."
-#: includes/class-m365-login-admin.php:346
+#: includes/class-m365-login-admin.php:374
+msgid "The sync has not run yet."
+msgstr "Der Sync ist noch nicht gelaufen."
+
+#: includes/class-m365-login-admin.php:378
+msgid "Finished"
+msgstr "Abgeschlossen"
+
+#: includes/class-m365-login-admin.php:379
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+#: includes/class-m365-login-admin.php:380
+msgid "Stopped by the safety limit"
+msgstr "Vom Sicherheitslimit gestoppt"
+
+#: includes/class-m365-login-admin.php:381
+msgid "Not started"
+msgstr "Nicht gestartet"
+
+#: includes/class-m365-login-admin.php:384
+msgid "started manually"
+msgstr "manuell gestartet"
+
+#: includes/class-m365-login-admin.php:385
+msgid "scheduled"
+msgstr "geplant"
+
+#: includes/class-m365-login-admin.php:386
+msgid "WP-CLI"
+msgstr "WP-CLI"
+
+#: includes/class-m365-login-admin.php:389
+msgid "would be created"
+msgstr "würden angelegt"
+
+#: includes/class-m365-login-admin.php:389
+msgid "created"
+msgstr "angelegt"
+
+#: includes/class-m365-login-admin.php:390
+msgid "would be updated"
+msgstr "würden aktualisiert"
+
+#: includes/class-m365-login-admin.php:390
+msgid "updated"
+msgstr "aktualisiert"
+
+#: includes/class-m365-login-admin.php:391
+msgid "would be linked"
+msgstr "würden verknüpft"
+
+#: includes/class-m365-login-admin.php:391
+msgid "linked"
+msgstr "verknüpft"
+
+#: includes/class-m365-login-admin.php:392
+msgid "unchanged"
+msgstr "unverändert"
+
+#: includes/class-m365-login-admin.php:393
+msgid "would be deactivated"
+msgstr "würden deaktiviert"
+
+#: includes/class-m365-login-admin.php:393
+msgid "deactivated"
+msgstr "deaktiviert"
+
+#: includes/class-m365-login-admin.php:394
+msgid "would be reactivated"
+msgstr "würden reaktiviert"
+
+#: includes/class-m365-login-admin.php:394
+msgid "reactivated"
+msgstr "reaktiviert"
+
+#: includes/class-m365-login-admin.php:395
+msgid "would be deleted"
+msgstr "würden gelöscht"
+
+#: includes/class-m365-login-admin.php:395
+msgid "deleted"
+msgstr "gelöscht"
+
+#: includes/class-m365-login-admin.php:396
+msgid "photos"
+msgstr "Profilbilder"
+
+#: includes/class-m365-login-admin.php:397
+msgid "skipped"
+msgstr "übersprungen"
+
+#: includes/class-m365-login-admin.php:398
+msgid "errors"
+msgstr "Fehler"
+
+#: includes/class-m365-login-admin.php:411
+msgid "Dry run – nothing was changed"
+msgstr "Testlauf – nichts wurde geändert"
+
+#. translators: 1: date and time, 2: how the run was started, 3: duration in seconds
+#: includes/class-m365-login-admin.php:416
+msgid "%1$s, %2$s, %3$d s"
+msgstr "%1$s, %2$s, %3$d s"
+
+#. translators: %d: number of log entries
+#: includes/class-m365-login-admin.php:434
+msgid "Log (%d entry)"
+msgid_plural "Log (%d entries)"
+msgstr[0] "Protokoll (%d Eintrag)"
+msgstr[1] "Protokoll (%d Einträge)"
+
+#: includes/class-m365-login-admin.php:462
+msgid "Search groups"
+msgstr "Gruppen suchen"
+
+#: includes/class-m365-login-admin.php:464
+msgid "Type a group name or paste an object ID…"
+msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
+
+#: includes/class-m365-login-admin.php:465
+msgid "Search"
+msgstr "Suchen"
+
+#: includes/class-m365-login-admin.php:470
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
+msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
+
+#: includes/class-m365-login-admin.php:476
+msgid "Selected groups"
+msgstr "Ausgewählte Gruppen"
+
+#: includes/class-m365-login-admin.php:488
+msgid "WordPress role"
+msgstr "WordPress-Rolle"
+
+#: includes/class-m365-login-admin.php:520
msgid "No certificate is stored."
msgstr "Es ist kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:364
+#: includes/class-m365-login-admin.php:545
+msgid "Do nothing"
+msgstr "Nichts tun"
+
+#: includes/class-m365-login-admin.php:546
+msgid "Deactivate the WordPress account"
+msgstr "WordPress-Konto deaktivieren"
+
+#: includes/class-m365-login-admin.php:547
+msgid "Delete the WordPress account"
+msgstr "WordPress-Konto löschen"
+
+#: includes/class-m365-login-admin.php:550
+msgid "Account disabled in Microsoft 365 (sign-in blocked)"
+msgstr "Konto in Microsoft 365 deaktiviert (Anmeldung blockiert)"
+
+#: includes/class-m365-login-admin.php:551
+msgid "Account deleted in Microsoft 365"
+msgstr "Konto in Microsoft 365 gelöscht"
+
+#: includes/class-m365-login-admin.php:552
+msgid "No longer a member of the sync groups"
+msgstr "Kein Mitglied der Sync-Gruppen mehr"
+
+#: includes/class-m365-login-admin.php:557
+msgid "Import users from Microsoft 365"
+msgstr "Benutzer aus Microsoft 365 importieren"
+
+#: includes/class-m365-login-admin.php:558
+msgid "Creates a WordPress account for every Microsoft 365 user in scope, links existing accounts by e-mail address, keeps roles and profile fields up to date and deactivates or deletes accounts that were disabled or removed in Microsoft 365. New accounts get a random password and no e-mail; people sign in with the Microsoft button."
+msgstr "Legt für jeden Microsoft-365-Benutzer im Geltungsbereich ein WordPress-Konto an, verknüpft bestehende Konten über die E-Mail-Adresse, hält Rollen und Profilfelder aktuell und deaktiviert oder löscht Konten, die in Microsoft 365 deaktiviert oder entfernt wurden. Neue Konten erhalten ein Zufallspasswort und keine E-Mail; die Anmeldung erfolgt über den Microsoft-Button."
+
+#: includes/class-m365-login-admin.php:563
+msgid "Run the sync automatically"
+msgstr "Sync automatisch ausführen"
+
+#: includes/class-m365-login-admin.php:564
+msgid "Uses WP-Cron, which runs when the site receives visits. For exact timing, trigger wp-cron.php from a real cron job or run \"wp m365-login sync\"."
+msgstr "Nutzt WP-Cron, das bei Besuchen der Website ausgelöst wird. Für genaue Zeiten rufe wp-cron.php über einen echten Cronjob auf oder führe „wp m365-login sync“ aus."
+
+#: includes/class-m365-login-admin.php:570
+msgid "Interval"
+msgstr "Intervall"
+
+#: includes/class-m365-login-admin.php:572
+msgid "Hourly"
+msgstr "Stündlich"
+
+#: includes/class-m365-login-admin.php:573
+msgid "Twice daily"
+msgstr "Zweimal täglich"
+
+#: includes/class-m365-login-admin.php:574
+msgid "Daily"
+msgstr "Täglich"
+
+#. translators: %s: date and time
+#: includes/class-m365-login-admin.php:578
+msgid "Next run: %s"
+msgstr "Nächster Lauf: %s"
+
+#: includes/class-m365-login-admin.php:586
+msgid "Also import guest users (B2B)"
+msgstr "Auch Gastbenutzer importieren (B2B)"
+
+#: includes/class-m365-login-admin.php:587
+msgid "Guests are external people invited into your tenant. Off by default."
+msgstr "Gäste sind externe Personen, die in deinen Tenant eingeladen wurden. Standardmäßig aus."
+
+#: includes/class-m365-login-admin.php:591
+msgid "Which users? (optional)"
+msgstr "Welche Benutzer? (optional)"
+
+#: includes/class-m365-login-admin.php:592
+msgid "Limit the import to members of these groups (nested memberships count). Without groups, every user of the tenant is imported. The e-mail domain allow-list on the Security tab applies as well."
+msgstr "Beschränkt den Import auf Mitglieder dieser Gruppen (verschachtelte Mitgliedschaften zählen). Ohne Gruppen wird jeder Benutzer des Tenants importiert. Die Liste erlaubter E-Mail-Domains im Tab „Sicherheit“ gilt ebenfalls."
+
+#: includes/class-m365-login-admin.php:593
+msgid "No groups selected – all users of the tenant are imported."
+msgstr "Keine Gruppen ausgewählt – alle Benutzer des Tenants werden importiert."
+
+#: includes/class-m365-login-admin.php:597
+msgid "Roles"
+msgstr "Rollen"
+
+#: includes/class-m365-login-admin.php:600
+msgid "Default role"
+msgstr "Standardrolle"
+
+#: includes/class-m365-login-admin.php:604
+msgid "Every imported user gets this role. The sync manages the roles of imported accounts – manual role changes are overwritten on the next run."
+msgstr "Jeder importierte Benutzer erhält diese Rolle. Die Rollen importierter Konten verwaltet der Sync – manuelle Rollenänderungen werden beim nächsten Lauf überschrieben."
+
+#: includes/class-m365-login-admin.php:607
+msgid "Additional roles from Microsoft 365 groups"
+msgstr "Zusätzliche Rollen aus Microsoft-365-Gruppen"
+
+#: includes/class-m365-login-admin.php:608
+msgid "Members of a group (nested memberships count) get the role next to it. If a person leaves the group, the role is removed again on the next sync."
+msgstr "Mitglieder einer Gruppe (verschachtelte Mitgliedschaften zählen) erhalten die Rolle daneben. Verlässt eine Person die Gruppe, wird die Rolle beim nächsten Sync wieder entfernt."
+
+#: includes/class-m365-login-admin.php:609
+msgid "No group mapping – everybody gets the default role."
+msgstr "Keine Gruppenzuordnung – alle erhalten die Standardrolle."
+
+#: includes/class-m365-login-admin.php:612
+msgid "How are mapped roles applied?"
+msgstr "Wie werden zugeordnete Rollen vergeben?"
+
+#: includes/class-m365-login-admin.php:615
+msgid "In addition to the default role (a user can have several roles)"
+msgstr "Zusätzlich zur Standardrolle (ein Benutzer kann mehrere Rollen haben)"
+
+#: includes/class-m365-login-admin.php:619
+msgid "Instead of the default role – the first matching group in the list wins (use ↑ to reorder)"
+msgstr "Anstelle der Standardrolle – die erste passende Gruppe der Liste gewinnt (Reihenfolge mit ↑ ändern)"
+
+#: includes/class-m365-login-admin.php:626
+msgid "Also manage the roles of accounts that existed before the sync"
+msgstr "Auch die Rollen von Konten verwalten, die schon vor dem Sync existierten"
+
+#: includes/class-m365-login-admin.php:627
+msgid "Off: existing accounts are only linked and get their profile fields updated; their roles stay as they are. Administrators that existed before the sync and your own account are never changed."
+msgstr "Aus: Bestehende Konten werden nur verknüpft und ihre Profilfelder aktualisiert; ihre Rollen bleiben, wie sie sind. Administratoren, die schon vor dem Sync existierten, und dein eigenes Konto werden nie verändert."
+
+#: includes/class-m365-login-admin.php:633
+msgid "Profile fields"
+msgstr "Profilfelder"
+
+#: includes/class-m365-login-admin.php:634
+msgid "Selected Microsoft 365 attributes are copied into the WordPress profile on every sync (Microsoft 365 wins). Name fields go into the standard profile fields, everything else into user meta keys starting with \"m365_\" – usable by themes and other plugins – and is shown on the profile screen."
+msgstr "Ausgewählte Microsoft-365-Attribute werden bei jedem Sync ins WordPress-Profil übernommen (Microsoft 365 hat Vorrang). Namen landen in den normalen Profilfeldern, alles andere in Benutzer-Metadaten mit dem Präfix „m365_“ – nutzbar für Themes und andere Plugins – und wird auf der Profilseite angezeigt."
+
+#: includes/class-m365-login-admin.php:643
+msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are checked about once a day per user."
+msgstr "Profilbilder werden in wp-content/uploads/m365-login-avatars/ gespeichert und ersetzen den Gravatar. Sie werden etwa einmal täglich pro Benutzer geprüft."
+
+#: includes/class-m365-login-admin.php:647
+msgid "Disabled and deleted Microsoft 365 accounts"
+msgstr "Deaktivierte und gelöschte Microsoft-365-Konten"
+
+#: includes/class-m365-login-admin.php:648
+msgid "Applies to WordPress accounts linked to a Microsoft account (imported, or signed in with Microsoft at least once). Deactivated accounts cannot sign in at all – not with Microsoft, a password or an application password – and are signed out immediately. When the person is active in Microsoft 365 again, the sync reactivates the account."
+msgstr "Gilt für WordPress-Konten, die mit einem Microsoft-Konto verknüpft sind (importiert oder mindestens einmal per Microsoft angemeldet). Deaktivierte Konten können sich gar nicht mehr anmelden – weder mit Microsoft noch mit Passwort oder Anwendungspasswort – und werden sofort abgemeldet. Ist die Person in Microsoft 365 wieder aktiv, reaktiviert der Sync das Konto."
+
+#: includes/class-m365-login-admin.php:659
+msgid "Only relevant when the import is limited to groups."
+msgstr "Nur relevant, wenn der Import auf Gruppen beschränkt ist."
+
+#: includes/class-m365-login-admin.php:665
+msgid "Posts of deleted accounts go to"
+msgstr "Beiträge gelöschter Konten übernimmt"
+
+#: includes/class-m365-login-admin.php:673
+msgid "— Select a user —"
+msgstr "— Benutzer auswählen —"
+
+#: includes/class-m365-login-admin.php:680
+msgid "Required for \"Delete\". Without a user, accounts are deactivated instead, so no content is ever lost."
+msgstr "Erforderlich für „Löschen“. Ohne Benutzer werden Konten stattdessen deaktiviert, damit nie Inhalte verloren gehen."
+
+#: includes/class-m365-login-admin.php:683
+msgid "Safety stop: if a run would deactivate or delete more than 20 % of the linked accounts (at least 5), nothing is deactivated or deleted and the run is reported as stopped. A failed Microsoft Graph request also stops the run before anything is deactivated."
+msgstr "Sicherheitsstopp: Würde ein Lauf mehr als 20 % der verknüpften Konten (mindestens 5) deaktivieren oder löschen, wird nichts deaktiviert oder gelöscht und der Lauf als gestoppt gemeldet. Auch eine fehlgeschlagene Microsoft-Graph-Anfrage stoppt den Lauf, bevor etwas deaktiviert wird."
+
+#: includes/class-m365-login-admin.php:687
+msgid "Run the sync"
+msgstr "Sync ausführen"
+
+#: includes/class-m365-login-admin.php:688
+msgid "The run uses the saved settings. Start with a dry run: it reads Microsoft 365 and lists what would change, without changing anything."
+msgstr "Der Lauf verwendet die gespeicherten Einstellungen. Beginne mit einem Testlauf: Er liest Microsoft 365 und listet auf, was sich ändern würde, ohne etwas zu ändern."
+
+#: includes/class-m365-login-admin.php:690
+msgid "Dry run"
+msgstr "Testlauf"
+
+#: includes/class-m365-login-admin.php:691
+msgid "Sync now"
+msgstr "Jetzt synchronisieren"
+
+#: includes/class-m365-login-admin.php:693
+msgid "Required application permissions (Microsoft Graph, admin consent): User.Read.All, and GroupMember.Read.All when groups are used."
+msgstr "Benötigte Anwendungsberechtigungen (Microsoft Graph, Administratorzustimmung): User.Read.All, bei Verwendung von Gruppen zusätzlich GroupMember.Read.All."
+
+#: includes/class-m365-login-admin.php:707
msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:383
+#: includes/class-m365-login-admin.php:726
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
-#: includes/class-m365-login-admin.php:388
+#: includes/class-m365-login-admin.php:731
msgid "Connected"
msgstr "Verbunden"
-#: includes/class-m365-login-admin.php:388
+#: includes/class-m365-login-admin.php:731
msgid "Setup incomplete"
msgstr "Einrichtung unvollständig"
-#: includes/class-m365-login-admin.php:410
+#: includes/class-m365-login-admin.php:753
msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID"
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:754
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "Trage hier die Werte aus deiner App-Registrierung im Microsoft Entra Admin Center ein."
-#: includes/class-m365-login-admin.php:414
+#: includes/class-m365-login-admin.php:757
msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)"
-#: includes/class-m365-login-admin.php:417
+#: includes/class-m365-login-admin.php:760
msgid "Test tenant"
msgstr "Tenant testen"
-#: includes/class-m365-login-admin.php:419
+#: includes/class-m365-login-admin.php:762
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr "Empfohlen: die GUID deines Tenants. Dann werden nur Anmeldungen aus diesem Tenant akzeptiert. „organizations“ erlaubt beliebige Geschäfts-, Schul- oder Unikonten."
-#: includes/class-m365-login-admin.php:421
+#: includes/class-m365-login-admin.php:764
msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
msgstr "Multi-Tenant-Modus: Konten aus beliebigen Microsoft-Tenants können sich anmelden. Deren „email“-Attribut ist nicht verifiziert, deshalb ordnet das Plugin nur über den User Principal Name (verifizierte Domain) zu und ignoriert den E-Mail-Claim, sofern Microsoft ihn nicht als domain-verifiziert markiert. Nutze die Domain-Allowlist im Tab „Sicherheit“ oder besser: die Tenant-GUID eintragen."
-#: includes/class-m365-login-admin.php:427
+#: includes/class-m365-login-admin.php:770
msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)"
-#: includes/class-m365-login-admin.php:432
+#: includes/class-m365-login-admin.php:775
msgid "How should WordPress authenticate to Microsoft?"
msgstr "Wie soll sich WordPress bei Microsoft authentifizieren?"
-#: includes/class-m365-login-admin.php:437 includes/class-m365-login-admin.php:454
+#: includes/class-m365-login-admin.php:780 includes/class-m365-login-admin.php:797
msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)"
-#: includes/class-m365-login-admin.php:438
+#: includes/class-m365-login-admin.php:781
msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
msgstr "Schnell eingerichtet. Ein passwortähnlicher Wert aus Entra ID, der nach 6–24 Monaten abläuft und erneuert werden muss."
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:787
msgid "Certificate"
msgstr "Zertifikat"
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:787
msgid "Recommended"
msgstr "Empfohlen"
-#: includes/class-m365-login-admin.php:445
+#: includes/class-m365-login-admin.php:788
msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
msgstr "Der private Schlüssel verlässt diesen Server nie; nur das öffentliche Zertifikat wird in Entra ID hochgeladen. Mit einem Klick hier erzeugt, 2 Jahre gültig."
-#: includes/class-m365-login-admin.php:456
+#: includes/class-m365-login-admin.php:799
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)"
-#: includes/class-m365-login-admin.php:456
+#: includes/class-m365-login-admin.php:799
msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen"
-#: includes/class-m365-login-admin.php:457
+#: includes/class-m365-login-admin.php:800
msgid "Show secret"
msgstr "Secret anzeigen"
-#: includes/class-m365-login-admin.php:462
+#: includes/class-m365-login-admin.php:805
msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen"
-#: includes/class-m365-login-admin.php:465
+#: includes/class-m365-login-admin.php:808
msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
msgstr "Wird verschlüsselt gespeichert (AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet) und nie wieder angezeigt. Client Secrets laufen ab – Ablaufdatum in Entra ID notieren."
-#: includes/class-m365-login-admin.php:469
+#: includes/class-m365-login-admin.php:812
msgid "Step-by-step: create a client secret in Entra ID"
msgstr "Schritt für Schritt: Client Secret in Entra ID erstellen"
-#: includes/class-m365-login-admin.php:472
+#: includes/class-m365-login-admin.php:815
msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
msgstr "entra.microsoft.com öffnen und mit einem Konto anmelden, das die Rolle „Anwendungsadministrator“ oder „Globaler Administrator“ hat."
-#: includes/class-m365-login-admin.php:473
+#: includes/class-m365-login-admin.php:816
msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
msgstr "Zu Identität → Anwendungen → App-Registrierungen wechseln und die App öffnen (oder zuerst anlegen, siehe allgemeine Anleitung in der Seitenleiste)."
-#: includes/class-m365-login-admin.php:474
+#: includes/class-m365-login-admin.php:817
msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Geheime Clientschlüssel“ und auf „Neuer geheimer Clientschlüssel“ klicken."
-#: includes/class-m365-login-admin.php:475
+#: includes/class-m365-login-admin.php:818
msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
msgstr "Eine Beschreibung wie „WordPress Login“ eingeben und eine Gültigkeit wählen. Microsoft erlaubt maximal 24 Monate; zwei Wochen vor Ablauf eine Kalender-Erinnerung setzen."
-#: includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:819
msgid "Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need."
msgstr "Auf „Hinzufügen“ klicken. Die Spalte „Wert“ sofort kopieren – sie wird nur einmal angezeigt. Die Spalte „Geheimnis-ID“ ist NICHT der gesuchte Wert."
-#: includes/class-m365-login-admin.php:477
+#: includes/class-m365-login-admin.php:820
msgid "Paste the value into the Client secret field above and save this page."
msgstr "Den Wert oben in das Feld „Geheimer Clientschlüssel“ einfügen und diese Seite speichern."
-#: includes/class-m365-login-admin.php:479
+#: includes/class-m365-login-admin.php:822
msgid "When the secret expires, sign-ins fail with \"Could not complete the sign-in with Microsoft\". Create a new secret, paste it here, save, then delete the old one in Entra ID."
msgstr "Läuft das Secret ab, scheitern Anmeldungen mit „Die Anmeldung über Microsoft konnte nicht abgeschlossen werden“. Dann ein neues Secret erstellen, hier einfügen, speichern und das alte in Entra ID löschen."
-#: includes/class-m365-login-admin.php:492
+#: includes/class-m365-login-admin.php:835
msgid "Expired"
msgstr "Abgelaufen"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:496
+#: includes/class-m365-login-admin.php:839
msgid "Expires in %d days"
msgstr "Läuft in %d Tagen ab"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:499
+#: includes/class-m365-login-admin.php:842
msgid "Valid"
msgstr "Gültig"
-#: includes/class-m365-login-admin.php:504
+#: includes/class-m365-login-admin.php:847
msgid "Thumbprint (SHA-1)"
msgstr "Fingerabdruck (SHA-1)"
-#: includes/class-m365-login-admin.php:506
+#: includes/class-m365-login-admin.php:849
msgid "Subject"
msgstr "Antragsteller"
-#: includes/class-m365-login-admin.php:508
+#: includes/class-m365-login-admin.php:851
msgid "Key size"
msgstr "Schlüssellänge"
-#: includes/class-m365-login-admin.php:510
+#: includes/class-m365-login-admin.php:853
msgid "Valid until"
msgstr "Gültig bis"
-#: includes/class-m365-login-admin.php:514
+#: includes/class-m365-login-admin.php:857
msgid "Download certificate (.cer)"
msgstr "Zertifikat herunterladen (.cer)"
-#: includes/class-m365-login-admin.php:515
+#: includes/class-m365-login-admin.php:858
msgid "Generate new certificate"
msgstr "Neues Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:518
+#: includes/class-m365-login-admin.php:861
msgid "Remove certificate when saving"
msgstr "Zertifikat beim Speichern entfernen"
-#: includes/class-m365-login-admin.php:522
+#: includes/class-m365-login-admin.php:865
msgid "No certificate stored yet."
msgstr "Noch kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:524
+#: includes/class-m365-login-admin.php:867
msgid "Generate certificate"
msgstr "Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:525
+#: includes/class-m365-login-admin.php:868
msgid "3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable."
msgstr "3072 Bit RSA, selbstsigniert, 2 Jahre gültig. Der private Schlüssel wird verschlüsselt gespeichert und nie angezeigt oder zum Download angeboten."
-#: includes/class-m365-login-admin.php:529
+#: includes/class-m365-login-admin.php:872
msgid "Use your own certificate instead (paste PEM)"
msgstr "Stattdessen eigenes Zertifikat verwenden (PEM einfügen)"
-#: includes/class-m365-login-admin.php:532
+#: includes/class-m365-login-admin.php:875
msgid "Private key (PEM, unencrypted)"
msgstr "Privater Schlüssel (PEM, unverschlüsselt)"
-#: includes/class-m365-login-admin.php:536
+#: includes/class-m365-login-admin.php:879
msgid "Certificate (PEM)"
msgstr "Zertifikat (PEM)"
-#: includes/class-m365-login-admin.php:538
+#: includes/class-m365-login-admin.php:881
msgid "RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards."
msgstr "RSA, mindestens 2048 Bit. Beim Speichern wird das Paar geprüft und der Schlüssel verschlüsselt. Beide Felder bleiben danach leer."
-#: includes/class-m365-login-admin.php:544
+#: includes/class-m365-login-admin.php:887
msgid "Step-by-step: register the certificate in Entra ID"
msgstr "Schritt für Schritt: Zertifikat in Entra ID hinterlegen"
-#: includes/class-m365-login-admin.php:547
+#: includes/class-m365-login-admin.php:890
msgid "Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part."
msgstr "Oben auf „Zertifikat erzeugen“ klicken (oder ein eigenes einfügen). Danach „Zertifikat herunterladen (.cer)“ – die Datei enthält nur den öffentlichen Teil."
-#: includes/class-m365-login-admin.php:548
+#: includes/class-m365-login-admin.php:891
msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen öffnen und die App auswählen."
-#: includes/class-m365-login-admin.php:549
+#: includes/class-m365-login-admin.php:892
msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Zertifikate“ und auf „Zertifikat hochladen“ klicken."
-#: includes/class-m365-login-admin.php:550
+#: includes/class-m365-login-admin.php:893
msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add."
msgstr "Die heruntergeladene .cer-Datei auswählen, eine Beschreibung wie „WordPress Login“ eingeben und auf „Hinzufügen“ klicken."
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:894
msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly."
msgstr "Den in Entra ID angezeigten Fingerabdruck mit dem Fingerabdruck oben vergleichen – beide müssen exakt übereinstimmen."
-#: includes/class-m365-login-admin.php:552
+#: includes/class-m365-login-admin.php:895
msgid "Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now."
msgstr "Sicherstellen, dass oben „Zertifikat“ ausgewählt ist, und diese Seite speichern. War vorher ein Client Secret gespeichert, kann es jetzt in Entra ID gelöscht werden."
-#: includes/class-m365-login-admin.php:554
+#: includes/class-m365-login-admin.php:897
msgid "How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted."
msgstr "So funktioniert es: Für jede Token-Anfrage signiert WordPress ein kurzlebiges JWT (Client Assertion) mit dem privaten Schlüssel; Microsoft prüft es mit dem hochgeladenen Zertifikat. Es wird nie ein Geheimnis übertragen."
-#: includes/class-m365-login-admin.php:555
+#: includes/class-m365-login-admin.php:898
msgid "Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch."
msgstr "Vor Ablauf des Zertifikats: hier ein neues erzeugen, in Entra ID hochladen (beide dürfen gleichzeitig hinterlegt sein), speichern und danach das alte in Entra ID entfernen. Anmeldungen funktionieren während des Wechsels weiter."
-#: includes/class-m365-login-admin.php:561
+#: includes/class-m365-login-admin.php:904
msgid "Account prompt"
msgstr "Kontoauswahl"
-#: includes/class-m365-login-admin.php:563
+#: includes/class-m365-login-admin.php:906
msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:907
msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
-#: includes/class-m365-login-admin.php:565
+#: includes/class-m365-login-admin.php:908
msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
-#: includes/class-m365-login-admin.php:574
+#: includes/class-m365-login-admin.php:917
msgid "Appearance"
msgstr "Darstellung"
-#: includes/class-m365-login-admin.php:577
+#: includes/class-m365-login-admin.php:920
msgid "Live preview"
msgstr "Live-Vorschau"
-#: includes/class-m365-login-admin.php:591
+#: includes/class-m365-login-admin.php:934
msgid "Button text"
msgstr "Button-Text"
-#: includes/class-m365-login-admin.php:595
+#: includes/class-m365-login-admin.php:938
msgid "Divider text"
msgstr "Trennlinien-Text"
-#: includes/class-m365-login-admin.php:597
+#: includes/class-m365-login-admin.php:940
msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden."
-#: includes/class-m365-login-admin.php:602
+#: includes/class-m365-login-admin.php:945
msgid "Icon"
msgstr "Icon"
-#: includes/class-m365-login-admin.php:605
+#: includes/class-m365-login-admin.php:948
msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen"
-#: includes/class-m365-login-admin.php:616
+#: includes/class-m365-login-admin.php:959
msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo"
-#: includes/class-m365-login-admin.php:618
+#: includes/class-m365-login-admin.php:961
msgid "Choose from media library"
msgstr "Aus Mediathek wählen"
-#: includes/class-m365-login-admin.php:619
+#: includes/class-m365-login-admin.php:962
msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden"
-#: includes/class-m365-login-admin.php:621
+#: includes/class-m365-login-admin.php:964
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
-#: includes/class-m365-login-admin.php:629
+#: includes/class-m365-login-admin.php:972
msgid "Background"
msgstr "Hintergrund"
-#: includes/class-m365-login-admin.php:630
+#: includes/class-m365-login-admin.php:973
msgid "Background (hover)"
msgstr "Hintergrund (Hover)"
-#: includes/class-m365-login-admin.php:631
+#: includes/class-m365-login-admin.php:974
msgid "Text colour"
msgstr "Textfarbe"
-#: includes/class-m365-login-admin.php:632
+#: includes/class-m365-login-admin.php:975
msgid "Border"
msgstr "Rahmen"
-#: includes/class-m365-login-admin.php:645
+#: includes/class-m365-login-admin.php:988
msgid "Corner radius"
msgstr "Eckenradius"
-#: includes/class-m365-login-admin.php:649
+#: includes/class-m365-login-admin.php:992
msgid "Position on the login page"
msgstr "Position auf der Login-Seite"
-#: includes/class-m365-login-admin.php:651
+#: includes/class-m365-login-admin.php:994
msgid "Below the login form"
msgstr "Unter dem Login-Formular"
-#: includes/class-m365-login-admin.php:652
+#: includes/class-m365-login-admin.php:995
msgid "Above the login form"
msgstr "Über dem Login-Formular"
-#: includes/class-m365-login-admin.php:658
+#: includes/class-m365-login-admin.php:1001
msgid "Quick presets"
msgstr "Schnellauswahl"
-#: includes/class-m365-login-admin.php:659
+#: includes/class-m365-login-admin.php:1002
msgid "Microsoft dark"
msgstr "Microsoft dunkel"
-#: includes/class-m365-login-admin.php:660
+#: includes/class-m365-login-admin.php:1003
msgid "Microsoft light"
msgstr "Microsoft hell"
-#: includes/class-m365-login-admin.php:661
+#: includes/class-m365-login-admin.php:1004
msgid "Azure blue"
msgstr "Azure-Blau"
-#: includes/class-m365-login-admin.php:662
+#: includes/class-m365-login-admin.php:1005
msgid "WordPress blue"
msgstr "WordPress-Blau"
-#: includes/class-m365-login-admin.php:666
+#: includes/class-m365-login-admin.php:1009
msgid "Custom login page"
msgstr "Eigene Login-Seite"
-#: includes/class-m365-login-admin.php:667
+#: includes/class-m365-login-admin.php:1010
msgid "Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there."
msgstr "Eigene Login-Seite statt wp-login.php? Hier eintragen, damit Fehlermeldungen, der Fallback-Link und die Weiterleitung nach dem Abmelden dorthin zeigen."
-#: includes/class-m365-login-admin.php:670
+#: includes/class-m365-login-admin.php:1013
msgid "URL of your login page"
msgstr "URL der Login-Seite"
-#: includes/class-m365-login-admin.php:672
+#: includes/class-m365-login-admin.php:1015
msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr "Muss auf dieser Website liegen. Leer lassen, um wp-login.php zu verwenden."
-#: includes/class-m365-login-admin.php:678
+#: includes/class-m365-login-admin.php:1021
msgid "Add the button to every wp_login_form() form automatically"
msgstr "Button automatisch in jedes wp_login_form()-Formular einfügen"
-#: includes/class-m365-login-admin.php:679
+#: includes/class-m365-login-admin.php:1022
msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr "Deckt Themes und Plugins ab, die die WordPress-Login-Formularfunktion verwenden. Page-Builder-Widgets benötigen den Shortcode oder die Template-Funktion unten."
-#: includes/class-m365-login-admin.php:684
+#: includes/class-m365-login-admin.php:1027
msgid "Manual placement"
msgstr "Manuelle Platzierung"
-#: includes/class-m365-login-admin.php:685
+#: includes/class-m365-login-admin.php:1028
msgid "Shortcode (block editor, page builders):"
msgstr "Shortcode (Block-Editor, Page Builder):"
-#: includes/class-m365-login-admin.php:687
+#: includes/class-m365-login-admin.php:1030
msgid "Template function (theme files):"
msgstr "Template-Funktion (Theme-Dateien):"
-#: includes/class-m365-login-admin.php:689
+#: includes/class-m365-login-admin.php:1032
msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
msgstr "Beide zeigen die Fehlermeldungen des letzten Versuchs; mit m365_login_messages() lassen sie sich separat platzieren."
-#: includes/class-m365-login-admin.php:697
+#: includes/class-m365-login-admin.php:1040
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:698
-msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
-msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert."
+#: includes/class-m365-login-admin.php:1041
+msgid "Sign-in never creates users. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists – created by hand or imported by the user sync."
+msgstr "Die Anmeldung legt nie Benutzer an. Eine Microsoft-Anmeldung klappt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert – von Hand angelegt oder vom Benutzer-Sync importiert."
-#: includes/class-m365-login-admin.php:703
+#: includes/class-m365-login-admin.php:1046
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:704
+#: includes/class-m365-login-admin.php:1047
msgid "On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:711
+#: includes/class-m365-login-admin.php:1054
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:712
+#: includes/class-m365-login-admin.php:1055
msgid "If the token contains no \"email\" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:719
+#: includes/class-m365-login-admin.php:1062
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:720
+#: includes/class-m365-login-admin.php:1063
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:725
+#: includes/class-m365-login-admin.php:1068
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:727
+#: includes/class-m365-login-admin.php:1070
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:732
+#: includes/class-m365-login-admin.php:1075
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:733
+#: includes/class-m365-login-admin.php:1076
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:736
-msgid "Search groups"
-msgstr "Gruppen suchen"
-
-#: includes/class-m365-login-admin.php:738
-msgid "Type a group name or paste an object ID…"
-msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
-
-#: includes/class-m365-login-admin.php:739
-msgid "Search"
-msgstr "Suchen"
-
-#: includes/class-m365-login-admin.php:744
-msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
-msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
-
-#: includes/class-m365-login-admin.php:750
-msgid "Selected groups"
-msgstr "Ausgewählte Gruppen"
-
-#: includes/class-m365-login-admin.php:751
+#: includes/class-m365-login-admin.php:1078
msgid "No groups selected – every matched user may sign in."
msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
-#: includes/class-m365-login-admin.php:761
+#: includes/class-m365-login-admin.php:1080
msgid "Membership is read from the \"groups\" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission \"User.Read.All\" or \"Directory.Read.All\"). If neither works, the sign-in is refused."
msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
-#: includes/class-m365-login-admin.php:766
+#: includes/class-m365-login-admin.php:1085
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
-#: includes/class-m365-login-admin.php:767
+#: includes/class-m365-login-admin.php:1086
msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected."
msgstr "Blendet die Benutzername/Passwort-Felder aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede interaktive Passwort-Anmeldung auf der Website ab, auch in eigenen Login-Formularen. Anwendungspasswörter, REST, XML-RPC und WP-CLI sind nicht betroffen."
-#: includes/class-m365-login-admin.php:772
+#: includes/class-m365-login-admin.php:1091
msgid "Show only the Microsoft button on the login page"
msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
-#: includes/class-m365-login-admin.php:773
+#: includes/class-m365-login-admin.php:1092
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
-#: includes/class-m365-login-admin.php:778
+#: includes/class-m365-login-admin.php:1097
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
-#: includes/class-m365-login-admin.php:779
+#: includes/class-m365-login-admin.php:1098
msgid "Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular und kann sich dort mit Passwort anmelden. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:1106
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
-#: includes/class-m365-login-admin.php:790
+#: includes/class-m365-login-admin.php:1109
msgid "A key is generated automatically the first time you save these settings."
msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:796
+#: includes/class-m365-login-admin.php:1115
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:805
+#: includes/class-m365-login-admin.php:1124
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:807
+#: includes/class-m365-login-admin.php:1126
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:808
+#: includes/class-m365-login-admin.php:1127
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:809
+#: includes/class-m365-login-admin.php:1128
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:810
-msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
-msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
+#: includes/class-m365-login-admin.php:1129
+msgid "Client secret encrypted at rest; sign-in never creates accounts or changes passwords."
+msgstr "Client Secret verschlüsselt gespeichert; die Anmeldung legt nie Konten an und ändert keine Passwörter."
-#: includes/class-m365-login-admin.php:816
+#: includes/class-m365-login-admin.php:1137
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:822
+#: includes/class-m365-login-admin.php:1143
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:823
+#: includes/class-m365-login-admin.php:1144
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:829
+#: includes/class-m365-login-admin.php:1150
msgid "Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:832
+#: includes/class-m365-login-admin.php:1153
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:837
+#: includes/class-m365-login-admin.php:1158
msgid "Setup guide: app registration"
msgstr "Anleitung: App-Registrierung"
-#: includes/class-m365-login-admin.php:839
+#: includes/class-m365-login-admin.php:1160
msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen → Neue Registrierung öffnen."
-#: includes/class-m365-login-admin.php:840
+#: includes/class-m365-login-admin.php:1161
msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)."
msgstr "Name: z. B. „WordPress Login“. Unterstützte Kontotypen: „Nur Konten in diesem Organisationsverzeichnis“ (Single Tenant)."
-#: includes/class-m365-login-admin.php:841
+#: includes/class-m365-login-admin.php:1162
msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register."
msgstr "Umleitungs-URI: Plattform „Web“ wählen und die oben angezeigte URI einfügen. Dann auf „Registrieren“ klicken."
-#: includes/class-m365-login-admin.php:842
+#: includes/class-m365-login-admin.php:1163
msgid "On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab."
msgstr "Auf der Übersichtsseite die Anwendungs-ID (Client) und die Verzeichnis-ID (Mandant) in den Tab „Verbindung“ kopieren."
-#: includes/class-m365-login-admin.php:843
+#: includes/class-m365-login-admin.php:1164
msgid "Authentication: leave \"ID tokens\" unchecked (the plugin uses the authorization code flow) and \"Allow public client flows\" on No."
msgstr "Authentifizierung: „ID-Token“ nicht anhaken (das Plugin nutzt den Authorization Code Flow) und „Öffentliche Clientflows zulassen“ auf „Nein“ lassen."
-#: includes/class-m365-login-admin.php:844
+#: includes/class-m365-login-admin.php:1165
msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt."
msgstr "Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → „email“ anhaken → Hinzufügen. Die Rückfrage zur API-Berechtigung bestätigen."
-#: includes/class-m365-login-admin.php:845
+#: includes/class-m365-login-admin.php:1166
msgid "Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate)."
msgstr "Im Tab „Verbindung“ die Authentifizierungsmethode wählen und der zugehörigen Schritt-für-Schritt-Anleitung folgen (Client Secret oder Zertifikat)."
-#: includes/class-m365-login-admin.php:846
+#: includes/class-m365-login-admin.php:1167
msgid "Optional: restrict who may use the app under Enterprise applications → your app → Properties → \"Assignment required\" = Yes, then assign users/groups."
msgstr "Optional: Unter Unternehmensanwendungen → deine App → Eigenschaften → „Zuweisung erforderlich“ = Ja einschränken, wer die App nutzen darf, und dann Benutzer/Gruppen zuweisen."
-#: includes/class-m365-login-admin.php:848
+#: includes/class-m365-login-admin.php:1169
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:849
-msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
-msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
+#: includes/class-m365-login-admin.php:1170
+msgid "Optional, for group restrictions and the user sync: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
+msgstr "Optional für Gruppen-Beschränkungen und den Benutzer-Sync: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
-#: includes/class-m365-login-admin.php:853
+#: includes/class-m365-login-admin.php:1174
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:854
+#: includes/class-m365-login-admin.php:1175
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-admin.php:856
+#: includes/class-m365-login-admin.php:1177
msgid "More options on the Button tab under \"Custom login page\"."
msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
@@ -726,74 +1051,78 @@ msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
-#: includes/class-m365-login-auth.php:823
+#: includes/class-m365-login-auth.php:827
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
-#: includes/class-m365-login-auth.php:844 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:848 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:845
+#: includes/class-m365-login-auth.php:849
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:846
+#: includes/class-m365-login-auth.php:850
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:847
+#: includes/class-m365-login-auth.php:851
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:848
+#: includes/class-m365-login-auth.php:852
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:849
+#: includes/class-m365-login-auth.php:853
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:850
+#: includes/class-m365-login-auth.php:854
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:851
+#: includes/class-m365-login-auth.php:855
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:852
+#: includes/class-m365-login-auth.php:856
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:853
+#: includes/class-m365-login-auth.php:857
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:854
+#: includes/class-m365-login-auth.php:858
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-auth.php:855
+#: includes/class-m365-login-auth.php:859
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
-#: includes/class-m365-login-auth.php:856
+#: includes/class-m365-login-auth.php:860
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:857
+#: includes/class-m365-login-auth.php:861
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
-#: includes/class-m365-login-auth.php:858
+#: includes/class-m365-login-auth.php:862
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
-#: includes/class-m365-login-auth.php:859
+#: includes/class-m365-login-auth.php:863
msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again."
msgstr "Zu viele Anmeldeversuche von dieser Verbindung. Bitte ein paar Minuten warten und erneut versuchen."
+#: includes/class-m365-login-auth.php:864 includes/class-m365-login-sync.php:1404
+msgid "This account has been deactivated."
+msgstr "Dieses Konto wurde deaktiviert."
+
#: includes/class-m365-login-certificate.php:29
msgid "The PHP OpenSSL extension is not available."
msgstr "Die PHP-Erweiterung OpenSSL ist nicht verfügbar."
@@ -846,75 +1175,368 @@ msgstr "Das Zertifikat gehört nicht zu diesem privaten Schlüssel."
msgid "The certificate has already expired."
msgstr "Das Zertifikat ist bereits abgelaufen."
-#: includes/class-m365-login-graph.php:201
+#: includes/class-m365-login-graph.php:375
msgid "Group"
msgstr "Gruppe"
-#: includes/class-m365-login-graph.php:203
+#: includes/class-m365-login-graph.php:377
msgid "Security group"
msgstr "Sicherheitsgruppe"
-#: includes/class-m365-login-graph.php:205
+#: includes/class-m365-login-graph.php:379
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
-#: includes/class-m365-login-settings.php:47
+#: includes/class-m365-login-settings.php:54
msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft"
-#: includes/class-m365-login-settings.php:56
+#: includes/class-m365-login-settings.php:63
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:197 includes/class-m365-login-settings.php:445
+#: includes/class-m365-login-settings.php:225 includes/class-m365-login-settings.php:530
msgid "The private key could not be encrypted. Is the OpenSSL extension available?"
msgstr "Der private Schlüssel konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:391
+#: includes/class-m365-login-settings.php:476
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:399
+#: includes/class-m365-login-settings.php:484
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:411
+#: includes/class-m365-login-settings.php:496
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:415
+#: includes/class-m365-login-settings.php:500
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:435
+#: includes/class-m365-login-settings.php:520
msgid "Please paste both the private key and the certificate."
msgstr "Bitte sowohl den privaten Schlüssel als auch das Zertifikat einfügen."
-#: includes/class-m365-login-settings.php:437
+#: includes/class-m365-login-settings.php:522
msgid "The pasted key or certificate is too large."
msgstr "Der eingefügte Schlüssel oder das Zertifikat ist zu groß."
-#: includes/class-m365-login-settings.php:454
+#: includes/class-m365-login-settings.php:539
msgid "Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then."
msgstr "Zertifikats-Authentifizierung ist ausgewählt, aber es ist noch kein Zertifikat gespeichert. Eines erzeugen oder ein eigenes einfügen; bis dahin bleibt der Microsoft-Button ausgeblendet."
-#: includes/class-m365-login-settings.php:500
+#: includes/class-m365-login-settings.php:571
msgid "The custom login page must be a URL on this site."
msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
-#: includes/class-m365-login.php:102
+#: includes/class-m365-login-settings.php:678
+msgid "User sync: \"Delete\" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead."
+msgstr "Benutzer-Sync: „Löschen“ braucht einen Benutzer, der die Beiträge gelöschter Konten übernimmt. Bis einer ausgewählt ist, werden Konten stattdessen deaktiviert."
+
+#: includes/class-m365-login-sync.php:109
+msgid "Display name"
+msgstr "Anzeigename"
+
+#: includes/class-m365-login-sync.php:113
+msgid "First name"
+msgstr "Vorname"
+
+#: includes/class-m365-login-sync.php:117
+msgid "Last name"
+msgstr "Nachname"
+
+#: includes/class-m365-login-sync.php:121
+msgid "Profile photo (used as avatar)"
+msgstr "Profilbild (als Avatar)"
+
+#: includes/class-m365-login-sync.php:125
+msgid "Job title"
+msgstr "Position"
+
+#: includes/class-m365-login-sync.php:129
+msgid "Department"
+msgstr "Abteilung"
+
+#: includes/class-m365-login-sync.php:133
+msgid "Company"
+msgstr "Firma"
+
+#: includes/class-m365-login-sync.php:137
+msgid "Office"
+msgstr "Büro"
+
+#: includes/class-m365-login-sync.php:141
+msgid "Employee ID"
+msgstr "Personalnummer"
+
+#: includes/class-m365-login-sync.php:145
+msgid "Business phone"
+msgstr "Telefon (geschäftlich)"
+
+#: includes/class-m365-login-sync.php:149
+msgid "Mobile phone"
+msgstr "Mobiltelefon"
+
+#: includes/class-m365-login-sync.php:153
+msgid "Street address"
+msgstr "Straße"
+
+#: includes/class-m365-login-sync.php:157
+msgid "Postal code"
+msgstr "Postleitzahl"
+
+#: includes/class-m365-login-sync.php:161
+msgid "City"
+msgstr "Ort"
+
+#: includes/class-m365-login-sync.php:165
+msgid "State / province"
+msgstr "Bundesland / Region"
+
+#: includes/class-m365-login-sync.php:169
+msgid "Country"
+msgstr "Land"
+
+#: includes/class-m365-login-sync.php:173
+msgid "Language (sets the admin language if installed)"
+msgstr "Sprache (setzt die Backend-Sprache, falls installiert)"
+
+#: includes/class-m365-login-sync.php:308
+msgid "Another sync is still running. Please try again in a few minutes."
+msgstr "Ein anderer Sync läuft noch. Bitte versuche es in ein paar Minuten erneut."
+
+#: includes/class-m365-login-sync.php:365
+msgid "The connection to Microsoft Entra ID is not configured yet."
+msgstr "Die Verbindung zu Microsoft Entra ID ist noch nicht eingerichtet."
+
+#: includes/class-m365-login-sync.php:369
+msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
+msgstr "Der Benutzer-Sync braucht eine feste Tenant-ID (GUID) im Tab „Verbindung“."
+
+#: includes/class-m365-login-sync.php:373
+msgid "The default role does not exist. Please check the sync settings."
+msgstr "Die Standardrolle existiert nicht. Bitte prüfe die Sync-Einstellungen."
+
+#. translators: %d: number of users
+#: includes/class-m365-login-sync.php:385
+msgid "%d user read from Microsoft 365."
+msgid_plural "%d users read from Microsoft 365."
+msgstr[0] "%d Benutzer aus Microsoft 365 gelesen."
+msgstr[1] "%d Benutzer aus Microsoft 365 gelesen."
+
+#. translators: 1: number of accounts, 2: limit
+#: includes/class-m365-login-sync.php:435
+msgid "Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter)."
+msgstr "Sicherheitsstopp: %1$d Konten würden deaktiviert oder gelöscht, mehr als das Limit von %2$d pro Lauf. Es wurde kein Konto deaktiviert oder gelöscht. Prüfe die Sync-Gruppen und den Tenant und starte den Sync dann erneut (das Limit lässt sich mit dem Filter m365_login_sync_deprovision_limit ändern)."
+
+#. translators: %s: user principal name
+#: includes/class-m365-login-sync.php:559
+msgid "%s: no usable e-mail address, skipped."
+msgstr "%s: keine verwendbare E-Mail-Adresse, übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:566
+msgid "%s: e-mail domain is not on the allow-list, skipped."
+msgstr "%s: E-Mail-Domain steht nicht auf der Liste erlaubter Domains, übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:578
+msgid "%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped."
+msgstr "%s: Das WordPress-Konto mit dieser E-Mail-Adresse ist mit einem anderen Microsoft-Konto verknüpft, übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:582
+msgid "%s: existing account linked."
+msgstr "%s: bestehendes Konto verknüpft."
+
+#: includes/class-m365-login-sync.php:596 includes/class-m365-login-sync.php:931 includes/class-m365-login-sync.php:1536
+msgid "disabled in Microsoft 365"
+msgstr "in Microsoft 365 deaktiviert"
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:607
+msgid "%s: added to this site."
+msgstr "%s: zu dieser Website hinzugefügt."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:616
+msgid "%s: reactivated (active in Microsoft 365 again)."
+msgstr "%s: reaktiviert (in Microsoft 365 wieder aktiv)."
+
+#. translators: 1: e-mail address, 2: list of changed fields
+#: includes/class-m365-login-sync.php:627
+msgid "%1$s: updated (%2$s)."
+msgstr "%1$s: aktualisiert (%2$s)."
+
+#. translators: 1: e-mail address, 2: role names
+#: includes/class-m365-login-sync.php:653
+msgid "%1$s: account created (%2$s)."
+msgstr "%1$s: Konto angelegt (%2$s)."
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:680
+msgid "%1$s: account could not be created: %2$s"
+msgstr "%1$s: Konto konnte nicht angelegt werden: %2$s"
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:738
+msgid "%s: e-mail address is used by another WordPress account and was not changed."
+msgstr "%s: Die E-Mail-Adresse gehört bereits einem anderen WordPress-Konto und wurde nicht geändert."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:741
+msgid "e-mail"
+msgstr "E-Mail"
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:785
+msgid "%1$s: profile could not be updated: %2$s"
+msgstr "%1$s: Profil konnte nicht aktualisiert werden: %2$s"
+
+#. translators: %s: role names
+#: includes/class-m365-login-sync.php:891
+msgid "roles: %s"
+msgstr "Rollen: %s"
+
+#: includes/class-m365-login-sync.php:926 includes/class-m365-login-sync.php:1537
+msgid "deleted in Microsoft 365"
+msgstr "in Microsoft 365 gelöscht"
+
+#: includes/class-m365-login-sync.php:934 includes/class-m365-login-sync.php:1538
+msgid "no longer a member of the sync groups"
+msgstr "kein Mitglied der Sync-Gruppen mehr"
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:954
+msgid "%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed."
+msgstr "%1$s: %2$s, das Konto ist aber geschützt (Administrator oder dein eigenes Konto) und wurde nicht geändert."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:987
+msgid "%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted."
+msgstr "%s: Es ist kein gültiger Benutzer für die Übernahme der Inhalte ausgewählt, deshalb wird das Konto deaktiviert statt gelöscht."
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:996
+msgid "%1$s: account deleted (%2$s)."
+msgstr "%1$s: Konto gelöscht (%2$s)."
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:1006
+msgid "%1$s: account deactivated (%2$s)."
+msgstr "%1$s: Konto deaktiviert (%2$s)."
+
+#: includes/class-m365-login-sync.php:1085
+msgid "Microsoft Graph refused the request. Grant the application permissions \"User.Read.All\" and \"GroupMember.Read.All\" with admin consent in Entra ID."
+msgstr "Microsoft Graph hat die Anfrage abgelehnt. Erteile in Entra ID die Anwendungsberechtigungen „User.Read.All“ und „GroupMember.Read.All“ mit Administratorzustimmung."
+
+#. translators: %s: error message
+#: includes/class-m365-login-sync.php:1088
+msgid "Microsoft Graph error: %s"
+msgstr "Microsoft-Graph-Fehler: %s"
+
+#: includes/class-m365-login-sync.php:1106
+msgid "Log truncated."
+msgstr "Protokoll gekürzt."
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:1168
+msgid "%1$s: profile photo could not be read: %2$s"
+msgstr "%1$s: Profilbild konnte nicht gelesen werden: %2$s"
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1176
+msgid "%s: profile photo removed."
+msgstr "%s: Profilbild entfernt."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1192
+msgid "%s: profile photo could not be downloaded."
+msgstr "%s: Profilbild konnte nicht heruntergeladen werden."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1199
+msgid "%s: profile photo is not a valid image or could not be saved."
+msgstr "%s: Profilbild ist kein gültiges Bild oder konnte nicht gespeichert werden."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1216
+msgid "%s: profile photo updated."
+msgstr "%s: Profilbild aktualisiert."
+
+#: includes/class-m365-login-sync.php:1433 includes/class-m365-login-sync.php:1566
+msgid "Microsoft 365"
+msgstr "Microsoft 365"
+
+#: includes/class-m365-login-sync.php:1451
+msgid "Deactivated"
+msgstr "Deaktiviert"
+
+#: includes/class-m365-login-sync.php:1454
+msgid "Imported"
+msgstr "Importiert"
+
+#: includes/class-m365-login-sync.php:1456
+msgid "Linked"
+msgstr "Verknüpft"
+
+#: includes/class-m365-login-sync.php:1484
+msgid "Reactivate"
+msgstr "Reaktivieren"
+
+#: includes/class-m365-login-sync.php:1484
+msgid "Deactivate"
+msgstr "Deaktivieren"
+
+#: includes/class-m365-login-sync.php:1517
+msgid "The account has been deactivated and signed out everywhere."
+msgstr "Das Konto wurde deaktiviert und überall abgemeldet."
+
+#: includes/class-m365-login-sync.php:1518
+msgid "The account has been reactivated."
+msgstr "Das Konto wurde reaktiviert."
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1542
+msgid "Deactivated since %1$s (%2$s)"
+msgstr "Deaktiviert seit %1$s (%2$s)"
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1544
+msgid "manually"
+msgstr "manuell"
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1546
+msgid "Status"
+msgstr "Status"
+
+#: includes/class-m365-login-sync.php:1549
+msgid "Object ID"
+msgstr "Objekt-ID"
+
+#: includes/class-m365-login-sync.php:1553
+msgid "Last sync"
+msgstr "Letzter Sync"
+
+#: includes/class-m365-login-sync.php:1575
+msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
+msgstr "Diese Werte verwaltet der Microsoft-365-Benutzer-Sync; sie werden beim nächsten Lauf überschrieben."
+
+#: includes/class-m365-login.php:110
msgid "Settings"
msgstr "Einstellungen"
-#: includes/class-m365-login.php:113
+#: includes/class-m365-login.php:121
msgid "M365 Login requires PHP 7.4 or newer."
msgstr "M365 Login benötigt PHP 7.4 oder neuer."
-#: includes/class-m365-login.php:114 includes/class-m365-login.php:123
+#: includes/class-m365-login.php:122 includes/class-m365-login.php:131
msgid "Plugin activation failed"
msgstr "Plugin-Aktivierung fehlgeschlagen"
-#: includes/class-m365-login.php:122
+#: includes/class-m365-login.php:130
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)."
-
diff --git a/languages/m365-login-de_DE_formal.mo b/languages/m365-login-de_DE_formal.mo
index e5f63fa..f404ef3 100644
Binary files a/languages/m365-login-de_DE_formal.mo and b/languages/m365-login-de_DE_formal.mo differ
diff --git a/languages/m365-login-de_DE_formal.po b/languages/m365-login-de_DE_formal.po
index 326d9d9..eee0dbc 100644
--- a/languages/m365-login-de_DE_formal.po
+++ b/languages/m365-login-de_DE_formal.po
@@ -2,13 +2,13 @@
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
-"Project-Id-Version: M365 Login 1.0.0\n"
+"Project-Id-Version: M365 Login 1.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/friloo/wp-m365-login/issues\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-09-22T00:00:00+00:00\n"
-"PO-Revision-Date: 2026-09-22 12:00+0000\n"
+"POT-Creation-Date: 2026-09-23T00:00:00+00:00\n"
+"PO-Revision-Date: 2026-09-23 12:00+0000\n"
"Last-Translator: friloo\n"
"Language-Team: German\n"
"Language: de_DE_formal\n"
@@ -16,709 +16,1034 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:81 includes/class-m365-login-admin.php:82 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:382
+#: includes/class-m365-login-admin.php:92 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:104 includes/class-m365-login-admin.php:725
msgid "M365 Login"
msgstr "M365 Login"
-#: includes/class-m365-login-admin.php:108
+#: includes/class-m365-login-admin.php:119
msgid "Connection"
msgstr "Verbindung"
-#: includes/class-m365-login-admin.php:109
+#: includes/class-m365-login-admin.php:120
msgid "Button"
msgstr "Button"
-#: includes/class-m365-login-admin.php:110
+#: includes/class-m365-login-admin.php:121
msgid "Security"
msgstr "Sicherheit"
-#: includes/class-m365-login-admin.php:185
+#: includes/class-m365-login-admin.php:122
+msgid "User sync"
+msgstr "Benutzer-Sync"
+
+#: includes/class-m365-login-admin.php:197
msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
-#: includes/class-m365-login-admin.php:187
+#: includes/class-m365-login-admin.php:199
msgid "Open the settings"
msgstr "Einstellungen öffnen"
-#: includes/class-m365-login-admin.php:217
+#: includes/class-m365-login-admin.php:230
msgid "Choose button icon"
msgstr "Button-Icon auswählen"
-#: includes/class-m365-login-admin.php:218
+#: includes/class-m365-login-admin.php:231
msgid "Use this icon"
msgstr "Dieses Icon verwenden"
-#: includes/class-m365-login-admin.php:219
+#: includes/class-m365-login-admin.php:232
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:220 includes/class-m365-login-admin.php:505 includes/class-m365-login-admin.php:783 includes/class-m365-login-admin.php:826
+#: includes/class-m365-login-admin.php:233 includes/class-m365-login-admin.php:848 includes/class-m365-login-admin.php:1102 includes/class-m365-login-admin.php:1147
msgid "Copy"
msgstr "Kopieren"
-#: includes/class-m365-login-admin.php:221
+#: includes/class-m365-login-admin.php:234
msgid "Testing…"
msgstr "Wird geprüft …"
-#: includes/class-m365-login-admin.php:222
+#: includes/class-m365-login-admin.php:235
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
-#: includes/class-m365-login-admin.php:223
+#: includes/class-m365-login-admin.php:236
msgid "No groups found."
msgstr "Keine Gruppen gefunden."
-#: includes/class-m365-login-admin.php:224
+#: includes/class-m365-login-admin.php:237
msgid "Searching…"
msgstr "Suche läuft …"
-#: includes/class-m365-login-admin.php:225
+#: includes/class-m365-login-admin.php:238
msgid "Add"
msgstr "Hinzufügen"
-#: includes/class-m365-login-admin.php:226 includes/class-m365-login-admin.php:757
+#: includes/class-m365-login-admin.php:239 includes/class-m365-login-admin.php:495
msgid "Remove"
msgstr "Entfernen"
-#: includes/class-m365-login-admin.php:227 includes/class-m365-login-admin.php:285 includes/class-m365-login-admin.php:742
+#: includes/class-m365-login-admin.php:240 includes/class-m365-login-admin.php:303 includes/class-m365-login-admin.php:468
msgid "Save the connection settings first, then search for groups."
msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
-#: includes/class-m365-login-admin.php:228
+#: includes/class-m365-login-admin.php:241
msgid "Generate a new fallback key on save? The old link stops working."
msgstr "Beim Speichern einen neuen Fallback-Schlüssel erzeugen? Der alte Link funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:229
+#: includes/class-m365-login-admin.php:242
msgid "Generating a 3072-bit key pair, this takes a moment…"
msgstr "3072-Bit-Schlüsselpaar wird erzeugt, das dauert einen Moment …"
-#: includes/class-m365-login-admin.php:230
+#: includes/class-m365-login-admin.php:243
msgid "Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID."
msgstr "Gespeichertes Zertifikat ersetzen? Die Anmeldung funktioniert erst wieder, wenn das neue Zertifikat in Entra ID hochgeladen ist."
-#: includes/class-m365-login-admin.php:231
+#: includes/class-m365-login-admin.php:244
msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working."
msgstr "Gespeichertes Zertifikat beim Speichern entfernen? Die Anmeldung per Zertifikat funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:243 includes/class-m365-login-admin.php:282 includes/class-m365-login-admin.php:308 includes/class-m365-login-admin.php:340
+#: includes/class-m365-login-admin.php:245
+msgid "Sync is running, this can take a while for large directories…"
+msgstr "Sync läuft, bei großen Verzeichnissen kann das etwas dauern …"
+
+#: includes/class-m365-login-admin.php:246
+msgid "Run the sync now with the saved settings? Accounts are created, updated and possibly deactivated or deleted. Tip: run a dry run first."
+msgstr "Sync jetzt mit den gespeicherten Einstellungen ausführen? Konten werden angelegt, aktualisiert und eventuell deaktiviert oder gelöscht. Tipp: Führen Sie zuerst einen Testlauf aus."
+
+#: includes/class-m365-login-admin.php:247
+msgid "The request failed or timed out. Reload the page in a few minutes to see the report; for very large directories use \"wp m365-login sync\" (WP-CLI)."
+msgstr "Die Anfrage ist fehlgeschlagen oder hat zu lange gedauert. Laden Sie die Seite in ein paar Minuten neu, um den Bericht zu sehen; für sehr große Verzeichnisse nutzen Sie „wp m365-login sync“ (WP-CLI)."
+
+#: includes/class-m365-login-admin.php:248
+msgid "You have unsaved changes. The sync uses the saved settings – save first."
+msgstr "Sie haben ungespeicherte Änderungen. Der Sync verwendet die gespeicherten Einstellungen – speichern Sie zuerst."
+
+#: includes/class-m365-login-admin.php:249 includes/class-m365-login-admin.php:482
+msgid "Move up"
+msgstr "Nach oben"
+
+#: includes/class-m365-login-admin.php:261 includes/class-m365-login-admin.php:300 includes/class-m365-login-admin.php:326 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:514 includes/class-m365-login-sync.php:1495
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:248
+#: includes/class-m365-login-admin.php:266
msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:262
+#: includes/class-m365-login-admin.php:280
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:271
+#: includes/class-m365-login-admin.php:289
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
-#: includes/class-m365-login-admin.php:294
+#: includes/class-m365-login-admin.php:312
msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr "Microsoft Graph hat die Anfrage abgelehnt. In Entra ID die Anwendungsberechtigung „GroupMember.Read.All“ (oder „Directory.Read.All“) mit Administratorzustimmung erteilen."
-#: includes/class-m365-login-admin.php:312
+#: includes/class-m365-login-admin.php:330
msgid "Unknown operation."
msgstr "Unbekannte Aktion."
-#: includes/class-m365-login-admin.php:329
+#: includes/class-m365-login-admin.php:347
msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID."
msgstr "Zertifikat erzeugt und gespeichert. Jetzt die .cer-Datei herunterladen und in Entra ID hochladen."
-#: includes/class-m365-login-admin.php:346
+#: includes/class-m365-login-admin.php:374
+msgid "The sync has not run yet."
+msgstr "Der Sync ist noch nicht gelaufen."
+
+#: includes/class-m365-login-admin.php:378
+msgid "Finished"
+msgstr "Abgeschlossen"
+
+#: includes/class-m365-login-admin.php:379
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+#: includes/class-m365-login-admin.php:380
+msgid "Stopped by the safety limit"
+msgstr "Vom Sicherheitslimit gestoppt"
+
+#: includes/class-m365-login-admin.php:381
+msgid "Not started"
+msgstr "Nicht gestartet"
+
+#: includes/class-m365-login-admin.php:384
+msgid "started manually"
+msgstr "manuell gestartet"
+
+#: includes/class-m365-login-admin.php:385
+msgid "scheduled"
+msgstr "geplant"
+
+#: includes/class-m365-login-admin.php:386
+msgid "WP-CLI"
+msgstr "WP-CLI"
+
+#: includes/class-m365-login-admin.php:389
+msgid "would be created"
+msgstr "würden angelegt"
+
+#: includes/class-m365-login-admin.php:389
+msgid "created"
+msgstr "angelegt"
+
+#: includes/class-m365-login-admin.php:390
+msgid "would be updated"
+msgstr "würden aktualisiert"
+
+#: includes/class-m365-login-admin.php:390
+msgid "updated"
+msgstr "aktualisiert"
+
+#: includes/class-m365-login-admin.php:391
+msgid "would be linked"
+msgstr "würden verknüpft"
+
+#: includes/class-m365-login-admin.php:391
+msgid "linked"
+msgstr "verknüpft"
+
+#: includes/class-m365-login-admin.php:392
+msgid "unchanged"
+msgstr "unverändert"
+
+#: includes/class-m365-login-admin.php:393
+msgid "would be deactivated"
+msgstr "würden deaktiviert"
+
+#: includes/class-m365-login-admin.php:393
+msgid "deactivated"
+msgstr "deaktiviert"
+
+#: includes/class-m365-login-admin.php:394
+msgid "would be reactivated"
+msgstr "würden reaktiviert"
+
+#: includes/class-m365-login-admin.php:394
+msgid "reactivated"
+msgstr "reaktiviert"
+
+#: includes/class-m365-login-admin.php:395
+msgid "would be deleted"
+msgstr "würden gelöscht"
+
+#: includes/class-m365-login-admin.php:395
+msgid "deleted"
+msgstr "gelöscht"
+
+#: includes/class-m365-login-admin.php:396
+msgid "photos"
+msgstr "Profilbilder"
+
+#: includes/class-m365-login-admin.php:397
+msgid "skipped"
+msgstr "übersprungen"
+
+#: includes/class-m365-login-admin.php:398
+msgid "errors"
+msgstr "Fehler"
+
+#: includes/class-m365-login-admin.php:411
+msgid "Dry run – nothing was changed"
+msgstr "Testlauf – nichts wurde geändert"
+
+#. translators: 1: date and time, 2: how the run was started, 3: duration in seconds
+#: includes/class-m365-login-admin.php:416
+msgid "%1$s, %2$s, %3$d s"
+msgstr "%1$s, %2$s, %3$d s"
+
+#. translators: %d: number of log entries
+#: includes/class-m365-login-admin.php:434
+msgid "Log (%d entry)"
+msgid_plural "Log (%d entries)"
+msgstr[0] "Protokoll (%d Eintrag)"
+msgstr[1] "Protokoll (%d Einträge)"
+
+#: includes/class-m365-login-admin.php:462
+msgid "Search groups"
+msgstr "Gruppen suchen"
+
+#: includes/class-m365-login-admin.php:464
+msgid "Type a group name or paste an object ID…"
+msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
+
+#: includes/class-m365-login-admin.php:465
+msgid "Search"
+msgstr "Suchen"
+
+#: includes/class-m365-login-admin.php:470
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
+msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
+
+#: includes/class-m365-login-admin.php:476
+msgid "Selected groups"
+msgstr "Ausgewählte Gruppen"
+
+#: includes/class-m365-login-admin.php:488
+msgid "WordPress role"
+msgstr "WordPress-Rolle"
+
+#: includes/class-m365-login-admin.php:520
msgid "No certificate is stored."
msgstr "Es ist kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:364
+#: includes/class-m365-login-admin.php:545
+msgid "Do nothing"
+msgstr "Nichts tun"
+
+#: includes/class-m365-login-admin.php:546
+msgid "Deactivate the WordPress account"
+msgstr "WordPress-Konto deaktivieren"
+
+#: includes/class-m365-login-admin.php:547
+msgid "Delete the WordPress account"
+msgstr "WordPress-Konto löschen"
+
+#: includes/class-m365-login-admin.php:550
+msgid "Account disabled in Microsoft 365 (sign-in blocked)"
+msgstr "Konto in Microsoft 365 deaktiviert (Anmeldung blockiert)"
+
+#: includes/class-m365-login-admin.php:551
+msgid "Account deleted in Microsoft 365"
+msgstr "Konto in Microsoft 365 gelöscht"
+
+#: includes/class-m365-login-admin.php:552
+msgid "No longer a member of the sync groups"
+msgstr "Kein Mitglied der Sync-Gruppen mehr"
+
+#: includes/class-m365-login-admin.php:557
+msgid "Import users from Microsoft 365"
+msgstr "Benutzer aus Microsoft 365 importieren"
+
+#: includes/class-m365-login-admin.php:558
+msgid "Creates a WordPress account for every Microsoft 365 user in scope, links existing accounts by e-mail address, keeps roles and profile fields up to date and deactivates or deletes accounts that were disabled or removed in Microsoft 365. New accounts get a random password and no e-mail; people sign in with the Microsoft button."
+msgstr "Legt für jeden Microsoft-365-Benutzer im Geltungsbereich ein WordPress-Konto an, verknüpft bestehende Konten über die E-Mail-Adresse, hält Rollen und Profilfelder aktuell und deaktiviert oder löscht Konten, die in Microsoft 365 deaktiviert oder entfernt wurden. Neue Konten erhalten ein Zufallspasswort und keine E-Mail; die Anmeldung erfolgt über den Microsoft-Button."
+
+#: includes/class-m365-login-admin.php:563
+msgid "Run the sync automatically"
+msgstr "Sync automatisch ausführen"
+
+#: includes/class-m365-login-admin.php:564
+msgid "Uses WP-Cron, which runs when the site receives visits. For exact timing, trigger wp-cron.php from a real cron job or run \"wp m365-login sync\"."
+msgstr "Nutzt WP-Cron, das bei Besuchen der Website ausgelöst wird. Für genaue Zeiten rufen Sie wp-cron.php über einen echten Cronjob auf oder führen Sie „wp m365-login sync“ aus."
+
+#: includes/class-m365-login-admin.php:570
+msgid "Interval"
+msgstr "Intervall"
+
+#: includes/class-m365-login-admin.php:572
+msgid "Hourly"
+msgstr "Stündlich"
+
+#: includes/class-m365-login-admin.php:573
+msgid "Twice daily"
+msgstr "Zweimal täglich"
+
+#: includes/class-m365-login-admin.php:574
+msgid "Daily"
+msgstr "Täglich"
+
+#. translators: %s: date and time
+#: includes/class-m365-login-admin.php:578
+msgid "Next run: %s"
+msgstr "Nächster Lauf: %s"
+
+#: includes/class-m365-login-admin.php:586
+msgid "Also import guest users (B2B)"
+msgstr "Auch Gastbenutzer importieren (B2B)"
+
+#: includes/class-m365-login-admin.php:587
+msgid "Guests are external people invited into your tenant. Off by default."
+msgstr "Gäste sind externe Personen, die in Ihren Tenant eingeladen wurden. Standardmäßig aus."
+
+#: includes/class-m365-login-admin.php:591
+msgid "Which users? (optional)"
+msgstr "Welche Benutzer? (optional)"
+
+#: includes/class-m365-login-admin.php:592
+msgid "Limit the import to members of these groups (nested memberships count). Without groups, every user of the tenant is imported. The e-mail domain allow-list on the Security tab applies as well."
+msgstr "Beschränkt den Import auf Mitglieder dieser Gruppen (verschachtelte Mitgliedschaften zählen). Ohne Gruppen wird jeder Benutzer des Tenants importiert. Die Liste erlaubter E-Mail-Domains im Tab „Sicherheit“ gilt ebenfalls."
+
+#: includes/class-m365-login-admin.php:593
+msgid "No groups selected – all users of the tenant are imported."
+msgstr "Keine Gruppen ausgewählt – alle Benutzer des Tenants werden importiert."
+
+#: includes/class-m365-login-admin.php:597
+msgid "Roles"
+msgstr "Rollen"
+
+#: includes/class-m365-login-admin.php:600
+msgid "Default role"
+msgstr "Standardrolle"
+
+#: includes/class-m365-login-admin.php:604
+msgid "Every imported user gets this role. The sync manages the roles of imported accounts – manual role changes are overwritten on the next run."
+msgstr "Jeder importierte Benutzer erhält diese Rolle. Die Rollen importierter Konten verwaltet der Sync – manuelle Rollenänderungen werden beim nächsten Lauf überschrieben."
+
+#: includes/class-m365-login-admin.php:607
+msgid "Additional roles from Microsoft 365 groups"
+msgstr "Zusätzliche Rollen aus Microsoft-365-Gruppen"
+
+#: includes/class-m365-login-admin.php:608
+msgid "Members of a group (nested memberships count) get the role next to it. If a person leaves the group, the role is removed again on the next sync."
+msgstr "Mitglieder einer Gruppe (verschachtelte Mitgliedschaften zählen) erhalten die Rolle daneben. Verlässt eine Person die Gruppe, wird die Rolle beim nächsten Sync wieder entfernt."
+
+#: includes/class-m365-login-admin.php:609
+msgid "No group mapping – everybody gets the default role."
+msgstr "Keine Gruppenzuordnung – alle erhalten die Standardrolle."
+
+#: includes/class-m365-login-admin.php:612
+msgid "How are mapped roles applied?"
+msgstr "Wie werden zugeordnete Rollen vergeben?"
+
+#: includes/class-m365-login-admin.php:615
+msgid "In addition to the default role (a user can have several roles)"
+msgstr "Zusätzlich zur Standardrolle (ein Benutzer kann mehrere Rollen haben)"
+
+#: includes/class-m365-login-admin.php:619
+msgid "Instead of the default role – the first matching group in the list wins (use ↑ to reorder)"
+msgstr "Anstelle der Standardrolle – die erste passende Gruppe der Liste gewinnt (Reihenfolge mit ↑ ändern)"
+
+#: includes/class-m365-login-admin.php:626
+msgid "Also manage the roles of accounts that existed before the sync"
+msgstr "Auch die Rollen von Konten verwalten, die schon vor dem Sync existierten"
+
+#: includes/class-m365-login-admin.php:627
+msgid "Off: existing accounts are only linked and get their profile fields updated; their roles stay as they are. Administrators that existed before the sync and your own account are never changed."
+msgstr "Aus: Bestehende Konten werden nur verknüpft und ihre Profilfelder aktualisiert; ihre Rollen bleiben, wie sie sind. Administratoren, die schon vor dem Sync existierten, und Ihr eigenes Konto werden nie verändert."
+
+#: includes/class-m365-login-admin.php:633
+msgid "Profile fields"
+msgstr "Profilfelder"
+
+#: includes/class-m365-login-admin.php:634
+msgid "Selected Microsoft 365 attributes are copied into the WordPress profile on every sync (Microsoft 365 wins). Name fields go into the standard profile fields, everything else into user meta keys starting with \"m365_\" – usable by themes and other plugins – and is shown on the profile screen."
+msgstr "Ausgewählte Microsoft-365-Attribute werden bei jedem Sync ins WordPress-Profil übernommen (Microsoft 365 hat Vorrang). Namen landen in den normalen Profilfeldern, alles andere in Benutzer-Metadaten mit dem Präfix „m365_“ – nutzbar für Themes und andere Plugins – und wird auf der Profilseite angezeigt."
+
+#: includes/class-m365-login-admin.php:643
+msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are checked about once a day per user."
+msgstr "Profilbilder werden in wp-content/uploads/m365-login-avatars/ gespeichert und ersetzen den Gravatar. Sie werden etwa einmal täglich pro Benutzer geprüft."
+
+#: includes/class-m365-login-admin.php:647
+msgid "Disabled and deleted Microsoft 365 accounts"
+msgstr "Deaktivierte und gelöschte Microsoft-365-Konten"
+
+#: includes/class-m365-login-admin.php:648
+msgid "Applies to WordPress accounts linked to a Microsoft account (imported, or signed in with Microsoft at least once). Deactivated accounts cannot sign in at all – not with Microsoft, a password or an application password – and are signed out immediately. When the person is active in Microsoft 365 again, the sync reactivates the account."
+msgstr "Gilt für WordPress-Konten, die mit einem Microsoft-Konto verknüpft sind (importiert oder mindestens einmal per Microsoft angemeldet). Deaktivierte Konten können sich gar nicht mehr anmelden – weder mit Microsoft noch mit Passwort oder Anwendungspasswort – und werden sofort abgemeldet. Ist die Person in Microsoft 365 wieder aktiv, reaktiviert der Sync das Konto."
+
+#: includes/class-m365-login-admin.php:659
+msgid "Only relevant when the import is limited to groups."
+msgstr "Nur relevant, wenn der Import auf Gruppen beschränkt ist."
+
+#: includes/class-m365-login-admin.php:665
+msgid "Posts of deleted accounts go to"
+msgstr "Beiträge gelöschter Konten übernimmt"
+
+#: includes/class-m365-login-admin.php:673
+msgid "— Select a user —"
+msgstr "— Benutzer auswählen —"
+
+#: includes/class-m365-login-admin.php:680
+msgid "Required for \"Delete\". Without a user, accounts are deactivated instead, so no content is ever lost."
+msgstr "Erforderlich für „Löschen“. Ohne Benutzer werden Konten stattdessen deaktiviert, damit nie Inhalte verloren gehen."
+
+#: includes/class-m365-login-admin.php:683
+msgid "Safety stop: if a run would deactivate or delete more than 20 % of the linked accounts (at least 5), nothing is deactivated or deleted and the run is reported as stopped. A failed Microsoft Graph request also stops the run before anything is deactivated."
+msgstr "Sicherheitsstopp: Würde ein Lauf mehr als 20 % der verknüpften Konten (mindestens 5) deaktivieren oder löschen, wird nichts deaktiviert oder gelöscht und der Lauf als gestoppt gemeldet. Auch eine fehlgeschlagene Microsoft-Graph-Anfrage stoppt den Lauf, bevor etwas deaktiviert wird."
+
+#: includes/class-m365-login-admin.php:687
+msgid "Run the sync"
+msgstr "Sync ausführen"
+
+#: includes/class-m365-login-admin.php:688
+msgid "The run uses the saved settings. Start with a dry run: it reads Microsoft 365 and lists what would change, without changing anything."
+msgstr "Der Lauf verwendet die gespeicherten Einstellungen. Beginnen Sie mit einem Testlauf: Er liest Microsoft 365 und listet auf, was sich ändern würde, ohne etwas zu ändern."
+
+#: includes/class-m365-login-admin.php:690
+msgid "Dry run"
+msgstr "Testlauf"
+
+#: includes/class-m365-login-admin.php:691
+msgid "Sync now"
+msgstr "Jetzt synchronisieren"
+
+#: includes/class-m365-login-admin.php:693
+msgid "Required application permissions (Microsoft Graph, admin consent): User.Read.All, and GroupMember.Read.All when groups are used."
+msgstr "Benötigte Anwendungsberechtigungen (Microsoft Graph, Administratorzustimmung): User.Read.All, bei Verwendung von Gruppen zusätzlich GroupMember.Read.All."
+
+#: includes/class-m365-login-admin.php:707
msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:383
+#: includes/class-m365-login-admin.php:726
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
-#: includes/class-m365-login-admin.php:388
+#: includes/class-m365-login-admin.php:731
msgid "Connected"
msgstr "Verbunden"
-#: includes/class-m365-login-admin.php:388
+#: includes/class-m365-login-admin.php:731
msgid "Setup incomplete"
msgstr "Einrichtung unvollständig"
-#: includes/class-m365-login-admin.php:410
+#: includes/class-m365-login-admin.php:753
msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID"
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:754
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "Tragen Sie hier die Werte aus Ihrer App-Registrierung im Microsoft Entra Admin Center ein."
-#: includes/class-m365-login-admin.php:414
+#: includes/class-m365-login-admin.php:757
msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)"
-#: includes/class-m365-login-admin.php:417
+#: includes/class-m365-login-admin.php:760
msgid "Test tenant"
msgstr "Tenant testen"
-#: includes/class-m365-login-admin.php:419
+#: includes/class-m365-login-admin.php:762
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr "Empfohlen: die GUID Ihres Tenants. Dann werden nur Anmeldungen aus diesem Tenant akzeptiert. „organizations“ erlaubt beliebige Geschäfts-, Schul- oder Unikonten."
-#: includes/class-m365-login-admin.php:421
+#: includes/class-m365-login-admin.php:764
msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
msgstr "Multi-Tenant-Modus: Konten aus beliebigen Microsoft-Tenants können sich anmelden. Deren „email“-Attribut ist nicht verifiziert, deshalb ordnet das Plugin nur über den User Principal Name (verifizierte Domain) zu und ignoriert den E-Mail-Claim, sofern Microsoft ihn nicht als domain-verifiziert markiert. Nutze die Domain-Allowlist im Tab „Sicherheit“ oder besser: die Tenant-GUID eintragen."
-#: includes/class-m365-login-admin.php:427
+#: includes/class-m365-login-admin.php:770
msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)"
-#: includes/class-m365-login-admin.php:432
+#: includes/class-m365-login-admin.php:775
msgid "How should WordPress authenticate to Microsoft?"
msgstr "Wie soll sich WordPress bei Microsoft authentifizieren?"
-#: includes/class-m365-login-admin.php:437 includes/class-m365-login-admin.php:454
+#: includes/class-m365-login-admin.php:780 includes/class-m365-login-admin.php:797
msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)"
-#: includes/class-m365-login-admin.php:438
+#: includes/class-m365-login-admin.php:781
msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
msgstr "Schnell eingerichtet. Ein passwortähnlicher Wert aus Entra ID, der nach 6–24 Monaten abläuft und erneuert werden muss."
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:787
msgid "Certificate"
msgstr "Zertifikat"
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:787
msgid "Recommended"
msgstr "Empfohlen"
-#: includes/class-m365-login-admin.php:445
+#: includes/class-m365-login-admin.php:788
msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
msgstr "Der private Schlüssel verlässt diesen Server nie; nur das öffentliche Zertifikat wird in Entra ID hochgeladen. Mit einem Klick hier erzeugt, 2 Jahre gültig."
-#: includes/class-m365-login-admin.php:456
+#: includes/class-m365-login-admin.php:799
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)"
-#: includes/class-m365-login-admin.php:456
+#: includes/class-m365-login-admin.php:799
msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen"
-#: includes/class-m365-login-admin.php:457
+#: includes/class-m365-login-admin.php:800
msgid "Show secret"
msgstr "Secret anzeigen"
-#: includes/class-m365-login-admin.php:462
+#: includes/class-m365-login-admin.php:805
msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen"
-#: includes/class-m365-login-admin.php:465
+#: includes/class-m365-login-admin.php:808
msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
msgstr "Wird verschlüsselt gespeichert (AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet) und nie wieder angezeigt. Client Secrets laufen ab – Ablaufdatum in Entra ID notieren."
-#: includes/class-m365-login-admin.php:469
+#: includes/class-m365-login-admin.php:812
msgid "Step-by-step: create a client secret in Entra ID"
msgstr "Schritt für Schritt: Client Secret in Entra ID erstellen"
-#: includes/class-m365-login-admin.php:472
+#: includes/class-m365-login-admin.php:815
msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
msgstr "entra.microsoft.com öffnen und mit einem Konto anmelden, das die Rolle „Anwendungsadministrator“ oder „Globaler Administrator“ hat."
-#: includes/class-m365-login-admin.php:473
+#: includes/class-m365-login-admin.php:816
msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
msgstr "Zu Identität → Anwendungen → App-Registrierungen wechseln und die App öffnen (oder zuerst anlegen, siehe allgemeine Anleitung in der Seitenleiste)."
-#: includes/class-m365-login-admin.php:474
+#: includes/class-m365-login-admin.php:817
msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Geheime Clientschlüssel“ und auf „Neuer geheimer Clientschlüssel“ klicken."
-#: includes/class-m365-login-admin.php:475
+#: includes/class-m365-login-admin.php:818
msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
msgstr "Eine Beschreibung wie „WordPress Login“ eingeben und eine Gültigkeit wählen. Microsoft erlaubt maximal 24 Monate; zwei Wochen vor Ablauf eine Kalender-Erinnerung setzen."
-#: includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:819
msgid "Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need."
msgstr "Auf „Hinzufügen“ klicken. Die Spalte „Wert“ sofort kopieren – sie wird nur einmal angezeigt. Die Spalte „Geheimnis-ID“ ist NICHT der gesuchte Wert."
-#: includes/class-m365-login-admin.php:477
+#: includes/class-m365-login-admin.php:820
msgid "Paste the value into the Client secret field above and save this page."
msgstr "Den Wert oben in das Feld „Geheimer Clientschlüssel“ einfügen und diese Seite speichern."
-#: includes/class-m365-login-admin.php:479
+#: includes/class-m365-login-admin.php:822
msgid "When the secret expires, sign-ins fail with \"Could not complete the sign-in with Microsoft\". Create a new secret, paste it here, save, then delete the old one in Entra ID."
msgstr "Läuft das Secret ab, scheitern Anmeldungen mit „Die Anmeldung über Microsoft konnte nicht abgeschlossen werden“. Dann ein neues Secret erstellen, hier einfügen, speichern und das alte in Entra ID löschen."
-#: includes/class-m365-login-admin.php:492
+#: includes/class-m365-login-admin.php:835
msgid "Expired"
msgstr "Abgelaufen"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:496
+#: includes/class-m365-login-admin.php:839
msgid "Expires in %d days"
msgstr "Läuft in %d Tagen ab"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:499
+#: includes/class-m365-login-admin.php:842
msgid "Valid"
msgstr "Gültig"
-#: includes/class-m365-login-admin.php:504
+#: includes/class-m365-login-admin.php:847
msgid "Thumbprint (SHA-1)"
msgstr "Fingerabdruck (SHA-1)"
-#: includes/class-m365-login-admin.php:506
+#: includes/class-m365-login-admin.php:849
msgid "Subject"
msgstr "Antragsteller"
-#: includes/class-m365-login-admin.php:508
+#: includes/class-m365-login-admin.php:851
msgid "Key size"
msgstr "Schlüssellänge"
-#: includes/class-m365-login-admin.php:510
+#: includes/class-m365-login-admin.php:853
msgid "Valid until"
msgstr "Gültig bis"
-#: includes/class-m365-login-admin.php:514
+#: includes/class-m365-login-admin.php:857
msgid "Download certificate (.cer)"
msgstr "Zertifikat herunterladen (.cer)"
-#: includes/class-m365-login-admin.php:515
+#: includes/class-m365-login-admin.php:858
msgid "Generate new certificate"
msgstr "Neues Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:518
+#: includes/class-m365-login-admin.php:861
msgid "Remove certificate when saving"
msgstr "Zertifikat beim Speichern entfernen"
-#: includes/class-m365-login-admin.php:522
+#: includes/class-m365-login-admin.php:865
msgid "No certificate stored yet."
msgstr "Noch kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:524
+#: includes/class-m365-login-admin.php:867
msgid "Generate certificate"
msgstr "Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:525
+#: includes/class-m365-login-admin.php:868
msgid "3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable."
msgstr "3072 Bit RSA, selbstsigniert, 2 Jahre gültig. Der private Schlüssel wird verschlüsselt gespeichert und nie angezeigt oder zum Download angeboten."
-#: includes/class-m365-login-admin.php:529
+#: includes/class-m365-login-admin.php:872
msgid "Use your own certificate instead (paste PEM)"
msgstr "Stattdessen eigenes Zertifikat verwenden (PEM einfügen)"
-#: includes/class-m365-login-admin.php:532
+#: includes/class-m365-login-admin.php:875
msgid "Private key (PEM, unencrypted)"
msgstr "Privater Schlüssel (PEM, unverschlüsselt)"
-#: includes/class-m365-login-admin.php:536
+#: includes/class-m365-login-admin.php:879
msgid "Certificate (PEM)"
msgstr "Zertifikat (PEM)"
-#: includes/class-m365-login-admin.php:538
+#: includes/class-m365-login-admin.php:881
msgid "RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards."
msgstr "RSA, mindestens 2048 Bit. Beim Speichern wird das Paar geprüft und der Schlüssel verschlüsselt. Beide Felder bleiben danach leer."
-#: includes/class-m365-login-admin.php:544
+#: includes/class-m365-login-admin.php:887
msgid "Step-by-step: register the certificate in Entra ID"
msgstr "Schritt für Schritt: Zertifikat in Entra ID hinterlegen"
-#: includes/class-m365-login-admin.php:547
+#: includes/class-m365-login-admin.php:890
msgid "Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part."
msgstr "Oben auf „Zertifikat erzeugen“ klicken (oder ein eigenes einfügen). Danach „Zertifikat herunterladen (.cer)“ – die Datei enthält nur den öffentlichen Teil."
-#: includes/class-m365-login-admin.php:548
+#: includes/class-m365-login-admin.php:891
msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen öffnen und die App auswählen."
-#: includes/class-m365-login-admin.php:549
+#: includes/class-m365-login-admin.php:892
msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Zertifikate“ und auf „Zertifikat hochladen“ klicken."
-#: includes/class-m365-login-admin.php:550
+#: includes/class-m365-login-admin.php:893
msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add."
msgstr "Die heruntergeladene .cer-Datei auswählen, eine Beschreibung wie „WordPress Login“ eingeben und auf „Hinzufügen“ klicken."
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:894
msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly."
msgstr "Den in Entra ID angezeigten Fingerabdruck mit dem Fingerabdruck oben vergleichen – beide müssen exakt übereinstimmen."
-#: includes/class-m365-login-admin.php:552
+#: includes/class-m365-login-admin.php:895
msgid "Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now."
msgstr "Sicherstellen, dass oben „Zertifikat“ ausgewählt ist, und diese Seite speichern. War vorher ein Client Secret gespeichert, kann es jetzt in Entra ID gelöscht werden."
-#: includes/class-m365-login-admin.php:554
+#: includes/class-m365-login-admin.php:897
msgid "How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted."
msgstr "So funktioniert es: Für jede Token-Anfrage signiert WordPress ein kurzlebiges JWT (Client Assertion) mit dem privaten Schlüssel; Microsoft prüft es mit dem hochgeladenen Zertifikat. Es wird nie ein Geheimnis übertragen."
-#: includes/class-m365-login-admin.php:555
+#: includes/class-m365-login-admin.php:898
msgid "Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch."
msgstr "Vor Ablauf des Zertifikats: hier ein neues erzeugen, in Entra ID hochladen (beide dürfen gleichzeitig hinterlegt sein), speichern und danach das alte in Entra ID entfernen. Anmeldungen funktionieren während des Wechsels weiter."
-#: includes/class-m365-login-admin.php:561
+#: includes/class-m365-login-admin.php:904
msgid "Account prompt"
msgstr "Kontoauswahl"
-#: includes/class-m365-login-admin.php:563
+#: includes/class-m365-login-admin.php:906
msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:907
msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
-#: includes/class-m365-login-admin.php:565
+#: includes/class-m365-login-admin.php:908
msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
-#: includes/class-m365-login-admin.php:574
+#: includes/class-m365-login-admin.php:917
msgid "Appearance"
msgstr "Darstellung"
-#: includes/class-m365-login-admin.php:577
+#: includes/class-m365-login-admin.php:920
msgid "Live preview"
msgstr "Live-Vorschau"
-#: includes/class-m365-login-admin.php:591
+#: includes/class-m365-login-admin.php:934
msgid "Button text"
msgstr "Button-Text"
-#: includes/class-m365-login-admin.php:595
+#: includes/class-m365-login-admin.php:938
msgid "Divider text"
msgstr "Trennlinien-Text"
-#: includes/class-m365-login-admin.php:597
+#: includes/class-m365-login-admin.php:940
msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden."
-#: includes/class-m365-login-admin.php:602
+#: includes/class-m365-login-admin.php:945
msgid "Icon"
msgstr "Icon"
-#: includes/class-m365-login-admin.php:605
+#: includes/class-m365-login-admin.php:948
msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen"
-#: includes/class-m365-login-admin.php:616
+#: includes/class-m365-login-admin.php:959
msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo"
-#: includes/class-m365-login-admin.php:618
+#: includes/class-m365-login-admin.php:961
msgid "Choose from media library"
msgstr "Aus Mediathek wählen"
-#: includes/class-m365-login-admin.php:619
+#: includes/class-m365-login-admin.php:962
msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden"
-#: includes/class-m365-login-admin.php:621
+#: includes/class-m365-login-admin.php:964
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
-#: includes/class-m365-login-admin.php:629
+#: includes/class-m365-login-admin.php:972
msgid "Background"
msgstr "Hintergrund"
-#: includes/class-m365-login-admin.php:630
+#: includes/class-m365-login-admin.php:973
msgid "Background (hover)"
msgstr "Hintergrund (Hover)"
-#: includes/class-m365-login-admin.php:631
+#: includes/class-m365-login-admin.php:974
msgid "Text colour"
msgstr "Textfarbe"
-#: includes/class-m365-login-admin.php:632
+#: includes/class-m365-login-admin.php:975
msgid "Border"
msgstr "Rahmen"
-#: includes/class-m365-login-admin.php:645
+#: includes/class-m365-login-admin.php:988
msgid "Corner radius"
msgstr "Eckenradius"
-#: includes/class-m365-login-admin.php:649
+#: includes/class-m365-login-admin.php:992
msgid "Position on the login page"
msgstr "Position auf der Login-Seite"
-#: includes/class-m365-login-admin.php:651
+#: includes/class-m365-login-admin.php:994
msgid "Below the login form"
msgstr "Unter dem Login-Formular"
-#: includes/class-m365-login-admin.php:652
+#: includes/class-m365-login-admin.php:995
msgid "Above the login form"
msgstr "Über dem Login-Formular"
-#: includes/class-m365-login-admin.php:658
+#: includes/class-m365-login-admin.php:1001
msgid "Quick presets"
msgstr "Schnellauswahl"
-#: includes/class-m365-login-admin.php:659
+#: includes/class-m365-login-admin.php:1002
msgid "Microsoft dark"
msgstr "Microsoft dunkel"
-#: includes/class-m365-login-admin.php:660
+#: includes/class-m365-login-admin.php:1003
msgid "Microsoft light"
msgstr "Microsoft hell"
-#: includes/class-m365-login-admin.php:661
+#: includes/class-m365-login-admin.php:1004
msgid "Azure blue"
msgstr "Azure-Blau"
-#: includes/class-m365-login-admin.php:662
+#: includes/class-m365-login-admin.php:1005
msgid "WordPress blue"
msgstr "WordPress-Blau"
-#: includes/class-m365-login-admin.php:666
+#: includes/class-m365-login-admin.php:1009
msgid "Custom login page"
msgstr "Eigene Login-Seite"
-#: includes/class-m365-login-admin.php:667
+#: includes/class-m365-login-admin.php:1010
msgid "Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there."
msgstr "Eigene Login-Seite statt wp-login.php? Hier eintragen, damit Fehlermeldungen, der Fallback-Link und die Weiterleitung nach dem Abmelden dorthin zeigen."
-#: includes/class-m365-login-admin.php:670
+#: includes/class-m365-login-admin.php:1013
msgid "URL of your login page"
msgstr "URL der Login-Seite"
-#: includes/class-m365-login-admin.php:672
+#: includes/class-m365-login-admin.php:1015
msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr "Muss auf dieser Website liegen. Leer lassen, um wp-login.php zu verwenden."
-#: includes/class-m365-login-admin.php:678
+#: includes/class-m365-login-admin.php:1021
msgid "Add the button to every wp_login_form() form automatically"
msgstr "Button automatisch in jedes wp_login_form()-Formular einfügen"
-#: includes/class-m365-login-admin.php:679
+#: includes/class-m365-login-admin.php:1022
msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr "Deckt Themes und Plugins ab, die die WordPress-Login-Formularfunktion verwenden. Page-Builder-Widgets benötigen den Shortcode oder die Template-Funktion unten."
-#: includes/class-m365-login-admin.php:684
+#: includes/class-m365-login-admin.php:1027
msgid "Manual placement"
msgstr "Manuelle Platzierung"
-#: includes/class-m365-login-admin.php:685
+#: includes/class-m365-login-admin.php:1028
msgid "Shortcode (block editor, page builders):"
msgstr "Shortcode (Block-Editor, Page Builder):"
-#: includes/class-m365-login-admin.php:687
+#: includes/class-m365-login-admin.php:1030
msgid "Template function (theme files):"
msgstr "Template-Funktion (Theme-Dateien):"
-#: includes/class-m365-login-admin.php:689
+#: includes/class-m365-login-admin.php:1032
msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
msgstr "Beide zeigen die Fehlermeldungen des letzten Versuchs; mit m365_login_messages() lassen sie sich separat platzieren."
-#: includes/class-m365-login-admin.php:697
+#: includes/class-m365-login-admin.php:1040
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:698
-msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
-msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert."
+#: includes/class-m365-login-admin.php:1041
+msgid "Sign-in never creates users. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists – created by hand or imported by the user sync."
+msgstr "Die Anmeldung legt nie Benutzer an. Eine Microsoft-Anmeldung klappt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert – von Hand angelegt oder vom Benutzer-Sync importiert."
-#: includes/class-m365-login-admin.php:703
+#: includes/class-m365-login-admin.php:1046
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:704
+#: includes/class-m365-login-admin.php:1047
msgid "On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:711
+#: includes/class-m365-login-admin.php:1054
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:712
+#: includes/class-m365-login-admin.php:1055
msgid "If the token contains no \"email\" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:719
+#: includes/class-m365-login-admin.php:1062
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:720
+#: includes/class-m365-login-admin.php:1063
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:725
+#: includes/class-m365-login-admin.php:1068
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:727
+#: includes/class-m365-login-admin.php:1070
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:732
+#: includes/class-m365-login-admin.php:1075
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:733
+#: includes/class-m365-login-admin.php:1076
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:736
-msgid "Search groups"
-msgstr "Gruppen suchen"
-
-#: includes/class-m365-login-admin.php:738
-msgid "Type a group name or paste an object ID…"
-msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
-
-#: includes/class-m365-login-admin.php:739
-msgid "Search"
-msgstr "Suchen"
-
-#: includes/class-m365-login-admin.php:744
-msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
-msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
-
-#: includes/class-m365-login-admin.php:750
-msgid "Selected groups"
-msgstr "Ausgewählte Gruppen"
-
-#: includes/class-m365-login-admin.php:751
+#: includes/class-m365-login-admin.php:1078
msgid "No groups selected – every matched user may sign in."
msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
-#: includes/class-m365-login-admin.php:761
+#: includes/class-m365-login-admin.php:1080
msgid "Membership is read from the \"groups\" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission \"User.Read.All\" or \"Directory.Read.All\"). If neither works, the sign-in is refused."
msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
-#: includes/class-m365-login-admin.php:766
+#: includes/class-m365-login-admin.php:1085
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
-#: includes/class-m365-login-admin.php:767
+#: includes/class-m365-login-admin.php:1086
msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected."
msgstr "Blendet die Benutzername/Passwort-Felder aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede interaktive Passwort-Anmeldung auf der Website ab, auch in eigenen Login-Formularen. Anwendungspasswörter, REST, XML-RPC und WP-CLI sind nicht betroffen."
-#: includes/class-m365-login-admin.php:772
+#: includes/class-m365-login-admin.php:1091
msgid "Show only the Microsoft button on the login page"
msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
-#: includes/class-m365-login-admin.php:773
+#: includes/class-m365-login-admin.php:1092
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
-#: includes/class-m365-login-admin.php:778
+#: includes/class-m365-login-admin.php:1097
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
-#: includes/class-m365-login-admin.php:779
+#: includes/class-m365-login-admin.php:1098
msgid "Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular und kann sich dort mit Passwort anmelden. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:1106
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
-#: includes/class-m365-login-admin.php:790
+#: includes/class-m365-login-admin.php:1109
msgid "A key is generated automatically the first time you save these settings."
msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:796
+#: includes/class-m365-login-admin.php:1115
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:805
+#: includes/class-m365-login-admin.php:1124
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:807
+#: includes/class-m365-login-admin.php:1126
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:808
+#: includes/class-m365-login-admin.php:1127
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:809
+#: includes/class-m365-login-admin.php:1128
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:810
-msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
-msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
+#: includes/class-m365-login-admin.php:1129
+msgid "Client secret encrypted at rest; sign-in never creates accounts or changes passwords."
+msgstr "Client Secret verschlüsselt gespeichert; die Anmeldung legt nie Konten an und ändert keine Passwörter."
-#: includes/class-m365-login-admin.php:816
+#: includes/class-m365-login-admin.php:1137
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:822
+#: includes/class-m365-login-admin.php:1143
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:823
+#: includes/class-m365-login-admin.php:1144
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:829
+#: includes/class-m365-login-admin.php:1150
msgid "Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:832
+#: includes/class-m365-login-admin.php:1153
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:837
+#: includes/class-m365-login-admin.php:1158
msgid "Setup guide: app registration"
msgstr "Anleitung: App-Registrierung"
-#: includes/class-m365-login-admin.php:839
+#: includes/class-m365-login-admin.php:1160
msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen → Neue Registrierung öffnen."
-#: includes/class-m365-login-admin.php:840
+#: includes/class-m365-login-admin.php:1161
msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)."
msgstr "Name: z. B. „WordPress Login“. Unterstützte Kontotypen: „Nur Konten in diesem Organisationsverzeichnis“ (Single Tenant)."
-#: includes/class-m365-login-admin.php:841
+#: includes/class-m365-login-admin.php:1162
msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register."
msgstr "Umleitungs-URI: Plattform „Web“ wählen und die oben angezeigte URI einfügen. Dann auf „Registrieren“ klicken."
-#: includes/class-m365-login-admin.php:842
+#: includes/class-m365-login-admin.php:1163
msgid "On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab."
msgstr "Auf der Übersichtsseite die Anwendungs-ID (Client) und die Verzeichnis-ID (Mandant) in den Tab „Verbindung“ kopieren."
-#: includes/class-m365-login-admin.php:843
+#: includes/class-m365-login-admin.php:1164
msgid "Authentication: leave \"ID tokens\" unchecked (the plugin uses the authorization code flow) and \"Allow public client flows\" on No."
msgstr "Authentifizierung: „ID-Token“ nicht anhaken (das Plugin nutzt den Authorization Code Flow) und „Öffentliche Clientflows zulassen“ auf „Nein“ lassen."
-#: includes/class-m365-login-admin.php:844
+#: includes/class-m365-login-admin.php:1165
msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt."
msgstr "Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → „email“ anhaken → Hinzufügen. Die Rückfrage zur API-Berechtigung bestätigen."
-#: includes/class-m365-login-admin.php:845
+#: includes/class-m365-login-admin.php:1166
msgid "Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate)."
msgstr "Im Tab „Verbindung“ die Authentifizierungsmethode wählen und der zugehörigen Schritt-für-Schritt-Anleitung folgen (Client Secret oder Zertifikat)."
-#: includes/class-m365-login-admin.php:846
+#: includes/class-m365-login-admin.php:1167
msgid "Optional: restrict who may use the app under Enterprise applications → your app → Properties → \"Assignment required\" = Yes, then assign users/groups."
msgstr "Optional: Unter Unternehmensanwendungen → Ihre App → Eigenschaften → „Zuweisung erforderlich“ = Ja einschränken, wer die App nutzen darf, und dann Benutzer/Gruppen zuweisen."
-#: includes/class-m365-login-admin.php:848
+#: includes/class-m365-login-admin.php:1169
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:849
-msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
-msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
+#: includes/class-m365-login-admin.php:1170
+msgid "Optional, for group restrictions and the user sync: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
+msgstr "Optional für Gruppen-Beschränkungen und den Benutzer-Sync: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
-#: includes/class-m365-login-admin.php:853
+#: includes/class-m365-login-admin.php:1174
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:854
+#: includes/class-m365-login-admin.php:1175
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-admin.php:856
+#: includes/class-m365-login-admin.php:1177
msgid "More options on the Button tab under \"Custom login page\"."
msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
@@ -726,74 +1051,78 @@ msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
-#: includes/class-m365-login-auth.php:823
+#: includes/class-m365-login-auth.php:827
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
-#: includes/class-m365-login-auth.php:844 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:848 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:845
+#: includes/class-m365-login-auth.php:849
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:846
+#: includes/class-m365-login-auth.php:850
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:847
+#: includes/class-m365-login-auth.php:851
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:848
+#: includes/class-m365-login-auth.php:852
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:849
+#: includes/class-m365-login-auth.php:853
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:850
+#: includes/class-m365-login-auth.php:854
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:851
+#: includes/class-m365-login-auth.php:855
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:852
+#: includes/class-m365-login-auth.php:856
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:853
+#: includes/class-m365-login-auth.php:857
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:854
+#: includes/class-m365-login-auth.php:858
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-auth.php:855
+#: includes/class-m365-login-auth.php:859
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
-#: includes/class-m365-login-auth.php:856
+#: includes/class-m365-login-auth.php:860
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:857
+#: includes/class-m365-login-auth.php:861
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
-#: includes/class-m365-login-auth.php:858
+#: includes/class-m365-login-auth.php:862
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
-#: includes/class-m365-login-auth.php:859
+#: includes/class-m365-login-auth.php:863
msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again."
msgstr "Zu viele Anmeldeversuche von dieser Verbindung. Bitte ein paar Minuten warten und erneut versuchen."
+#: includes/class-m365-login-auth.php:864 includes/class-m365-login-sync.php:1404
+msgid "This account has been deactivated."
+msgstr "Dieses Konto wurde deaktiviert."
+
#: includes/class-m365-login-certificate.php:29
msgid "The PHP OpenSSL extension is not available."
msgstr "Die PHP-Erweiterung OpenSSL ist nicht verfügbar."
@@ -846,75 +1175,368 @@ msgstr "Das Zertifikat gehört nicht zu diesem privaten Schlüssel."
msgid "The certificate has already expired."
msgstr "Das Zertifikat ist bereits abgelaufen."
-#: includes/class-m365-login-graph.php:201
+#: includes/class-m365-login-graph.php:375
msgid "Group"
msgstr "Gruppe"
-#: includes/class-m365-login-graph.php:203
+#: includes/class-m365-login-graph.php:377
msgid "Security group"
msgstr "Sicherheitsgruppe"
-#: includes/class-m365-login-graph.php:205
+#: includes/class-m365-login-graph.php:379
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
-#: includes/class-m365-login-settings.php:47
+#: includes/class-m365-login-settings.php:54
msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft"
-#: includes/class-m365-login-settings.php:56
+#: includes/class-m365-login-settings.php:63
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:197 includes/class-m365-login-settings.php:445
+#: includes/class-m365-login-settings.php:225 includes/class-m365-login-settings.php:530
msgid "The private key could not be encrypted. Is the OpenSSL extension available?"
msgstr "Der private Schlüssel konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:391
+#: includes/class-m365-login-settings.php:476
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:399
+#: includes/class-m365-login-settings.php:484
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:411
+#: includes/class-m365-login-settings.php:496
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:415
+#: includes/class-m365-login-settings.php:500
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:435
+#: includes/class-m365-login-settings.php:520
msgid "Please paste both the private key and the certificate."
msgstr "Bitte sowohl den privaten Schlüssel als auch das Zertifikat einfügen."
-#: includes/class-m365-login-settings.php:437
+#: includes/class-m365-login-settings.php:522
msgid "The pasted key or certificate is too large."
msgstr "Der eingefügte Schlüssel oder das Zertifikat ist zu groß."
-#: includes/class-m365-login-settings.php:454
+#: includes/class-m365-login-settings.php:539
msgid "Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then."
msgstr "Zertifikats-Authentifizierung ist ausgewählt, aber es ist noch kein Zertifikat gespeichert. Eines erzeugen oder ein eigenes einfügen; bis dahin bleibt der Microsoft-Button ausgeblendet."
-#: includes/class-m365-login-settings.php:500
+#: includes/class-m365-login-settings.php:571
msgid "The custom login page must be a URL on this site."
msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
-#: includes/class-m365-login.php:102
+#: includes/class-m365-login-settings.php:678
+msgid "User sync: \"Delete\" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead."
+msgstr "Benutzer-Sync: „Löschen“ braucht einen Benutzer, der die Beiträge gelöschter Konten übernimmt. Bis einer ausgewählt ist, werden Konten stattdessen deaktiviert."
+
+#: includes/class-m365-login-sync.php:109
+msgid "Display name"
+msgstr "Anzeigename"
+
+#: includes/class-m365-login-sync.php:113
+msgid "First name"
+msgstr "Vorname"
+
+#: includes/class-m365-login-sync.php:117
+msgid "Last name"
+msgstr "Nachname"
+
+#: includes/class-m365-login-sync.php:121
+msgid "Profile photo (used as avatar)"
+msgstr "Profilbild (als Avatar)"
+
+#: includes/class-m365-login-sync.php:125
+msgid "Job title"
+msgstr "Position"
+
+#: includes/class-m365-login-sync.php:129
+msgid "Department"
+msgstr "Abteilung"
+
+#: includes/class-m365-login-sync.php:133
+msgid "Company"
+msgstr "Firma"
+
+#: includes/class-m365-login-sync.php:137
+msgid "Office"
+msgstr "Büro"
+
+#: includes/class-m365-login-sync.php:141
+msgid "Employee ID"
+msgstr "Personalnummer"
+
+#: includes/class-m365-login-sync.php:145
+msgid "Business phone"
+msgstr "Telefon (geschäftlich)"
+
+#: includes/class-m365-login-sync.php:149
+msgid "Mobile phone"
+msgstr "Mobiltelefon"
+
+#: includes/class-m365-login-sync.php:153
+msgid "Street address"
+msgstr "Straße"
+
+#: includes/class-m365-login-sync.php:157
+msgid "Postal code"
+msgstr "Postleitzahl"
+
+#: includes/class-m365-login-sync.php:161
+msgid "City"
+msgstr "Ort"
+
+#: includes/class-m365-login-sync.php:165
+msgid "State / province"
+msgstr "Bundesland / Region"
+
+#: includes/class-m365-login-sync.php:169
+msgid "Country"
+msgstr "Land"
+
+#: includes/class-m365-login-sync.php:173
+msgid "Language (sets the admin language if installed)"
+msgstr "Sprache (setzt die Backend-Sprache, falls installiert)"
+
+#: includes/class-m365-login-sync.php:308
+msgid "Another sync is still running. Please try again in a few minutes."
+msgstr "Ein anderer Sync läuft noch. Bitte versuchen Sie es in ein paar Minuten erneut."
+
+#: includes/class-m365-login-sync.php:365
+msgid "The connection to Microsoft Entra ID is not configured yet."
+msgstr "Die Verbindung zu Microsoft Entra ID ist noch nicht eingerichtet."
+
+#: includes/class-m365-login-sync.php:369
+msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
+msgstr "Der Benutzer-Sync braucht eine feste Tenant-ID (GUID) im Tab „Verbindung“."
+
+#: includes/class-m365-login-sync.php:373
+msgid "The default role does not exist. Please check the sync settings."
+msgstr "Die Standardrolle existiert nicht. Bitte prüfen Sie die Sync-Einstellungen."
+
+#. translators: %d: number of users
+#: includes/class-m365-login-sync.php:385
+msgid "%d user read from Microsoft 365."
+msgid_plural "%d users read from Microsoft 365."
+msgstr[0] "%d Benutzer aus Microsoft 365 gelesen."
+msgstr[1] "%d Benutzer aus Microsoft 365 gelesen."
+
+#. translators: 1: number of accounts, 2: limit
+#: includes/class-m365-login-sync.php:435
+msgid "Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter)."
+msgstr "Sicherheitsstopp: %1$d Konten würden deaktiviert oder gelöscht, mehr als das Limit von %2$d pro Lauf. Es wurde kein Konto deaktiviert oder gelöscht. Prüfen Sie die Sync-Gruppen und den Tenant und starten Sie den Sync dann erneut (das Limit lässt sich mit dem Filter m365_login_sync_deprovision_limit ändern)."
+
+#. translators: %s: user principal name
+#: includes/class-m365-login-sync.php:559
+msgid "%s: no usable e-mail address, skipped."
+msgstr "%s: keine verwendbare E-Mail-Adresse, übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:566
+msgid "%s: e-mail domain is not on the allow-list, skipped."
+msgstr "%s: E-Mail-Domain steht nicht auf der Liste erlaubter Domains, übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:578
+msgid "%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped."
+msgstr "%s: Das WordPress-Konto mit dieser E-Mail-Adresse ist mit einem anderen Microsoft-Konto verknüpft, übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:582
+msgid "%s: existing account linked."
+msgstr "%s: bestehendes Konto verknüpft."
+
+#: includes/class-m365-login-sync.php:596 includes/class-m365-login-sync.php:931 includes/class-m365-login-sync.php:1536
+msgid "disabled in Microsoft 365"
+msgstr "in Microsoft 365 deaktiviert"
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:607
+msgid "%s: added to this site."
+msgstr "%s: zu dieser Website hinzugefügt."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:616
+msgid "%s: reactivated (active in Microsoft 365 again)."
+msgstr "%s: reaktiviert (in Microsoft 365 wieder aktiv)."
+
+#. translators: 1: e-mail address, 2: list of changed fields
+#: includes/class-m365-login-sync.php:627
+msgid "%1$s: updated (%2$s)."
+msgstr "%1$s: aktualisiert (%2$s)."
+
+#. translators: 1: e-mail address, 2: role names
+#: includes/class-m365-login-sync.php:653
+msgid "%1$s: account created (%2$s)."
+msgstr "%1$s: Konto angelegt (%2$s)."
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:680
+msgid "%1$s: account could not be created: %2$s"
+msgstr "%1$s: Konto konnte nicht angelegt werden: %2$s"
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:738
+msgid "%s: e-mail address is used by another WordPress account and was not changed."
+msgstr "%s: Die E-Mail-Adresse gehört bereits einem anderen WordPress-Konto und wurde nicht geändert."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:741
+msgid "e-mail"
+msgstr "E-Mail"
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:785
+msgid "%1$s: profile could not be updated: %2$s"
+msgstr "%1$s: Profil konnte nicht aktualisiert werden: %2$s"
+
+#. translators: %s: role names
+#: includes/class-m365-login-sync.php:891
+msgid "roles: %s"
+msgstr "Rollen: %s"
+
+#: includes/class-m365-login-sync.php:926 includes/class-m365-login-sync.php:1537
+msgid "deleted in Microsoft 365"
+msgstr "in Microsoft 365 gelöscht"
+
+#: includes/class-m365-login-sync.php:934 includes/class-m365-login-sync.php:1538
+msgid "no longer a member of the sync groups"
+msgstr "kein Mitglied der Sync-Gruppen mehr"
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:954
+msgid "%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed."
+msgstr "%1$s: %2$s, das Konto ist aber geschützt (Administrator oder Ihr eigenes Konto) und wurde nicht geändert."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:987
+msgid "%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted."
+msgstr "%s: Es ist kein gültiger Benutzer für die Übernahme der Inhalte ausgewählt, deshalb wird das Konto deaktiviert statt gelöscht."
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:996
+msgid "%1$s: account deleted (%2$s)."
+msgstr "%1$s: Konto gelöscht (%2$s)."
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:1006
+msgid "%1$s: account deactivated (%2$s)."
+msgstr "%1$s: Konto deaktiviert (%2$s)."
+
+#: includes/class-m365-login-sync.php:1085
+msgid "Microsoft Graph refused the request. Grant the application permissions \"User.Read.All\" and \"GroupMember.Read.All\" with admin consent in Entra ID."
+msgstr "Microsoft Graph hat die Anfrage abgelehnt. Erteilen Sie in Entra ID die Anwendungsberechtigungen „User.Read.All“ und „GroupMember.Read.All“ mit Administratorzustimmung."
+
+#. translators: %s: error message
+#: includes/class-m365-login-sync.php:1088
+msgid "Microsoft Graph error: %s"
+msgstr "Microsoft-Graph-Fehler: %s"
+
+#: includes/class-m365-login-sync.php:1106
+msgid "Log truncated."
+msgstr "Protokoll gekürzt."
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:1168
+msgid "%1$s: profile photo could not be read: %2$s"
+msgstr "%1$s: Profilbild konnte nicht gelesen werden: %2$s"
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1176
+msgid "%s: profile photo removed."
+msgstr "%s: Profilbild entfernt."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1192
+msgid "%s: profile photo could not be downloaded."
+msgstr "%s: Profilbild konnte nicht heruntergeladen werden."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1199
+msgid "%s: profile photo is not a valid image or could not be saved."
+msgstr "%s: Profilbild ist kein gültiges Bild oder konnte nicht gespeichert werden."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1216
+msgid "%s: profile photo updated."
+msgstr "%s: Profilbild aktualisiert."
+
+#: includes/class-m365-login-sync.php:1433 includes/class-m365-login-sync.php:1566
+msgid "Microsoft 365"
+msgstr "Microsoft 365"
+
+#: includes/class-m365-login-sync.php:1451
+msgid "Deactivated"
+msgstr "Deaktiviert"
+
+#: includes/class-m365-login-sync.php:1454
+msgid "Imported"
+msgstr "Importiert"
+
+#: includes/class-m365-login-sync.php:1456
+msgid "Linked"
+msgstr "Verknüpft"
+
+#: includes/class-m365-login-sync.php:1484
+msgid "Reactivate"
+msgstr "Reaktivieren"
+
+#: includes/class-m365-login-sync.php:1484
+msgid "Deactivate"
+msgstr "Deaktivieren"
+
+#: includes/class-m365-login-sync.php:1517
+msgid "The account has been deactivated and signed out everywhere."
+msgstr "Das Konto wurde deaktiviert und überall abgemeldet."
+
+#: includes/class-m365-login-sync.php:1518
+msgid "The account has been reactivated."
+msgstr "Das Konto wurde reaktiviert."
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1542
+msgid "Deactivated since %1$s (%2$s)"
+msgstr "Deaktiviert seit %1$s (%2$s)"
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1544
+msgid "manually"
+msgstr "manuell"
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1546
+msgid "Status"
+msgstr "Status"
+
+#: includes/class-m365-login-sync.php:1549
+msgid "Object ID"
+msgstr "Objekt-ID"
+
+#: includes/class-m365-login-sync.php:1553
+msgid "Last sync"
+msgstr "Letzter Sync"
+
+#: includes/class-m365-login-sync.php:1575
+msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
+msgstr "Diese Werte verwaltet der Microsoft-365-Benutzer-Sync; sie werden beim nächsten Lauf überschrieben."
+
+#: includes/class-m365-login.php:110
msgid "Settings"
msgstr "Einstellungen"
-#: includes/class-m365-login.php:113
+#: includes/class-m365-login.php:121
msgid "M365 Login requires PHP 7.4 or newer."
msgstr "M365 Login benötigt PHP 7.4 oder neuer."
-#: includes/class-m365-login.php:114 includes/class-m365-login.php:123
+#: includes/class-m365-login.php:122 includes/class-m365-login.php:131
msgid "Plugin activation failed"
msgstr "Plugin-Aktivierung fehlgeschlagen"
-#: includes/class-m365-login.php:122
+#: includes/class-m365-login.php:130
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)."
-
diff --git a/languages/m365-login.pot b/languages/m365-login.pot
index 48fd0bc..e861616 100644
--- a/languages/m365-login.pot
+++ b/languages/m365-login.pot
@@ -2,721 +2,1046 @@
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
-"Project-Id-Version: M365 Login 1.0.0\n"
+"Project-Id-Version: M365 Login 1.1.0\n"
"Report-Msgid-Bugs-To: https://github.com/friloo/wp-m365-login/issues\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"POT-Creation-Date: 2026-09-22T00:00:00+00:00\n"
+"POT-Creation-Date: 2026-09-23T00:00:00+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME
\n"
"Language-Team: LANGUAGE \n"
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:81 includes/class-m365-login-admin.php:82 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:382
+#: includes/class-m365-login-admin.php:92 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:104 includes/class-m365-login-admin.php:725
msgid "M365 Login"
msgstr ""
-#: includes/class-m365-login-admin.php:108
+#: includes/class-m365-login-admin.php:119
msgid "Connection"
msgstr ""
-#: includes/class-m365-login-admin.php:109
+#: includes/class-m365-login-admin.php:120
msgid "Button"
msgstr ""
-#: includes/class-m365-login-admin.php:110
+#: includes/class-m365-login-admin.php:121
msgid "Security"
msgstr ""
-#: includes/class-m365-login-admin.php:185
+#: includes/class-m365-login-admin.php:122
+msgid "User sync"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:197
msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr ""
-#: includes/class-m365-login-admin.php:187
+#: includes/class-m365-login-admin.php:199
msgid "Open the settings"
msgstr ""
-#: includes/class-m365-login-admin.php:217
+#: includes/class-m365-login-admin.php:230
msgid "Choose button icon"
msgstr ""
-#: includes/class-m365-login-admin.php:218
+#: includes/class-m365-login-admin.php:231
msgid "Use this icon"
msgstr ""
-#: includes/class-m365-login-admin.php:219
+#: includes/class-m365-login-admin.php:232
msgid "Copied!"
msgstr ""
-#: includes/class-m365-login-admin.php:220 includes/class-m365-login-admin.php:505 includes/class-m365-login-admin.php:783 includes/class-m365-login-admin.php:826
+#: includes/class-m365-login-admin.php:233 includes/class-m365-login-admin.php:848 includes/class-m365-login-admin.php:1102 includes/class-m365-login-admin.php:1147
msgid "Copy"
msgstr ""
-#: includes/class-m365-login-admin.php:221
+#: includes/class-m365-login-admin.php:234
msgid "Testing…"
msgstr ""
-#: includes/class-m365-login-admin.php:222
+#: includes/class-m365-login-admin.php:235
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr ""
-#: includes/class-m365-login-admin.php:223
+#: includes/class-m365-login-admin.php:236
msgid "No groups found."
msgstr ""
-#: includes/class-m365-login-admin.php:224
+#: includes/class-m365-login-admin.php:237
msgid "Searching…"
msgstr ""
-#: includes/class-m365-login-admin.php:225
+#: includes/class-m365-login-admin.php:238
msgid "Add"
msgstr ""
-#: includes/class-m365-login-admin.php:226 includes/class-m365-login-admin.php:757
+#: includes/class-m365-login-admin.php:239 includes/class-m365-login-admin.php:495
msgid "Remove"
msgstr ""
-#: includes/class-m365-login-admin.php:227 includes/class-m365-login-admin.php:285 includes/class-m365-login-admin.php:742
+#: includes/class-m365-login-admin.php:240 includes/class-m365-login-admin.php:303 includes/class-m365-login-admin.php:468
msgid "Save the connection settings first, then search for groups."
msgstr ""
-#: includes/class-m365-login-admin.php:228
+#: includes/class-m365-login-admin.php:241
msgid "Generate a new fallback key on save? The old link stops working."
msgstr ""
-#: includes/class-m365-login-admin.php:229
+#: includes/class-m365-login-admin.php:242
msgid "Generating a 3072-bit key pair, this takes a moment…"
msgstr ""
-#: includes/class-m365-login-admin.php:230
+#: includes/class-m365-login-admin.php:243
msgid "Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:231
+#: includes/class-m365-login-admin.php:244
msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working."
msgstr ""
-#: includes/class-m365-login-admin.php:243 includes/class-m365-login-admin.php:282 includes/class-m365-login-admin.php:308 includes/class-m365-login-admin.php:340
-msgid "You are not allowed to do this."
+#: includes/class-m365-login-admin.php:245
+msgid "Sync is running, this can take a while for large directories…"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:246
+msgid "Run the sync now with the saved settings? Accounts are created, updated and possibly deactivated or deleted. Tip: run a dry run first."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:247
+msgid "The request failed or timed out. Reload the page in a few minutes to see the report; for very large directories use \"wp m365-login sync\" (WP-CLI)."
msgstr ""
#: includes/class-m365-login-admin.php:248
+msgid "You have unsaved changes. The sync uses the saved settings – save first."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:249 includes/class-m365-login-admin.php:482
+msgid "Move up"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:261 includes/class-m365-login-admin.php:300 includes/class-m365-login-admin.php:326 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:514 includes/class-m365-login-sync.php:1495
+msgid "You are not allowed to do this."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:266
msgid "Please enter a valid tenant ID first."
msgstr ""
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:262
+#: includes/class-m365-login-admin.php:280
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr ""
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:271
+#: includes/class-m365-login-admin.php:289
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr ""
-#: includes/class-m365-login-admin.php:294
+#: includes/class-m365-login-admin.php:312
msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:312
+#: includes/class-m365-login-admin.php:330
msgid "Unknown operation."
msgstr ""
-#: includes/class-m365-login-admin.php:329
+#: includes/class-m365-login-admin.php:347
msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:346
-msgid "No certificate is stored."
+#: includes/class-m365-login-admin.php:374
+msgid "The sync has not run yet."
msgstr ""
-#: includes/class-m365-login-admin.php:364
-msgid "You are not allowed to access this page."
+#: includes/class-m365-login-admin.php:378
+msgid "Finished"
msgstr ""
-#: includes/class-m365-login-admin.php:383
-msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
+#: includes/class-m365-login-admin.php:379
+msgid "Failed"
msgstr ""
-#: includes/class-m365-login-admin.php:388
-msgid "Connected"
+#: includes/class-m365-login-admin.php:380
+msgid "Stopped by the safety limit"
msgstr ""
-#: includes/class-m365-login-admin.php:388
-msgid "Setup incomplete"
+#: includes/class-m365-login-admin.php:381
+msgid "Not started"
msgstr ""
-#: includes/class-m365-login-admin.php:410
-msgid "Microsoft Entra ID app registration"
+#: includes/class-m365-login-admin.php:384
+msgid "started manually"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:385
+msgid "scheduled"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:386
+msgid "WP-CLI"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:389
+msgid "would be created"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:389
+msgid "created"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:390
+msgid "would be updated"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:390
+msgid "updated"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:391
+msgid "would be linked"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:391
+msgid "linked"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:392
+msgid "unchanged"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:393
+msgid "would be deactivated"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:393
+msgid "deactivated"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:394
+msgid "would be reactivated"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:394
+msgid "reactivated"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:395
+msgid "would be deleted"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:395
+msgid "deleted"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:396
+msgid "photos"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:397
+msgid "skipped"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:398
+msgid "errors"
msgstr ""
#: includes/class-m365-login-admin.php:411
-msgid "Enter the values from your app registration in the Microsoft Entra admin center."
+msgid "Dry run – nothing was changed"
msgstr ""
-#: includes/class-m365-login-admin.php:414
-msgid "Directory (tenant) ID"
+#. translators: 1: date and time, 2: how the run was started, 3: duration in seconds
+#: includes/class-m365-login-admin.php:416
+msgid "%1$s, %2$s, %3$d s"
msgstr ""
-#: includes/class-m365-login-admin.php:417
-msgid "Test tenant"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:419
-msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:421
-msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:427
-msgid "Application (client) ID"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:432
-msgid "How should WordPress authenticate to Microsoft?"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:437 includes/class-m365-login-admin.php:454
-msgid "Client secret"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:438
-msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:444
-msgid "Certificate"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:444
-msgid "Recommended"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:445
-msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:456
-msgid "•••••••••••• (stored, leave empty to keep)"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:456
-msgid "Paste the secret value"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:457
-msgid "Show secret"
-msgstr ""
+#. translators: %d: number of log entries
+#: includes/class-m365-login-admin.php:434
+msgid "Log (%d entry)"
+msgid_plural "Log (%d entries)"
+msgstr[0] ""
+msgstr[1] ""
#: includes/class-m365-login-admin.php:462
-msgid "Remove the stored secret"
+msgid "Search groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:464
+msgid "Type a group name or paste an object ID…"
msgstr ""
#: includes/class-m365-login-admin.php:465
-msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
+msgid "Search"
msgstr ""
-#: includes/class-m365-login-admin.php:469
-msgid "Step-by-step: create a client secret in Entra ID"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:472
-msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:473
-msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:474
-msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:475
-msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
+#: includes/class-m365-login-admin.php:470
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr ""
#: includes/class-m365-login-admin.php:476
+msgid "Selected groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:488
+msgid "WordPress role"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:520
+msgid "No certificate is stored."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:545
+msgid "Do nothing"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:546
+msgid "Deactivate the WordPress account"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:547
+msgid "Delete the WordPress account"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:550
+msgid "Account disabled in Microsoft 365 (sign-in blocked)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:551
+msgid "Account deleted in Microsoft 365"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:552
+msgid "No longer a member of the sync groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:557
+msgid "Import users from Microsoft 365"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:558
+msgid "Creates a WordPress account for every Microsoft 365 user in scope, links existing accounts by e-mail address, keeps roles and profile fields up to date and deactivates or deletes accounts that were disabled or removed in Microsoft 365. New accounts get a random password and no e-mail; people sign in with the Microsoft button."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:563
+msgid "Run the sync automatically"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:564
+msgid "Uses WP-Cron, which runs when the site receives visits. For exact timing, trigger wp-cron.php from a real cron job or run \"wp m365-login sync\"."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:570
+msgid "Interval"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:572
+msgid "Hourly"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:573
+msgid "Twice daily"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:574
+msgid "Daily"
+msgstr ""
+
+#. translators: %s: date and time
+#: includes/class-m365-login-admin.php:578
+msgid "Next run: %s"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:586
+msgid "Also import guest users (B2B)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:587
+msgid "Guests are external people invited into your tenant. Off by default."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:591
+msgid "Which users? (optional)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:592
+msgid "Limit the import to members of these groups (nested memberships count). Without groups, every user of the tenant is imported. The e-mail domain allow-list on the Security tab applies as well."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:593
+msgid "No groups selected – all users of the tenant are imported."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:597
+msgid "Roles"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:600
+msgid "Default role"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:604
+msgid "Every imported user gets this role. The sync manages the roles of imported accounts – manual role changes are overwritten on the next run."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:607
+msgid "Additional roles from Microsoft 365 groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:608
+msgid "Members of a group (nested memberships count) get the role next to it. If a person leaves the group, the role is removed again on the next sync."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:609
+msgid "No group mapping – everybody gets the default role."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:612
+msgid "How are mapped roles applied?"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:615
+msgid "In addition to the default role (a user can have several roles)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:619
+msgid "Instead of the default role – the first matching group in the list wins (use ↑ to reorder)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:626
+msgid "Also manage the roles of accounts that existed before the sync"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:627
+msgid "Off: existing accounts are only linked and get their profile fields updated; their roles stay as they are. Administrators that existed before the sync and your own account are never changed."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:633
+msgid "Profile fields"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:634
+msgid "Selected Microsoft 365 attributes are copied into the WordPress profile on every sync (Microsoft 365 wins). Name fields go into the standard profile fields, everything else into user meta keys starting with \"m365_\" – usable by themes and other plugins – and is shown on the profile screen."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:643
+msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are checked about once a day per user."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:647
+msgid "Disabled and deleted Microsoft 365 accounts"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:648
+msgid "Applies to WordPress accounts linked to a Microsoft account (imported, or signed in with Microsoft at least once). Deactivated accounts cannot sign in at all – not with Microsoft, a password or an application password – and are signed out immediately. When the person is active in Microsoft 365 again, the sync reactivates the account."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:659
+msgid "Only relevant when the import is limited to groups."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:665
+msgid "Posts of deleted accounts go to"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:673
+msgid "— Select a user —"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:680
+msgid "Required for \"Delete\". Without a user, accounts are deactivated instead, so no content is ever lost."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:683
+msgid "Safety stop: if a run would deactivate or delete more than 20 % of the linked accounts (at least 5), nothing is deactivated or deleted and the run is reported as stopped. A failed Microsoft Graph request also stops the run before anything is deactivated."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:687
+msgid "Run the sync"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:688
+msgid "The run uses the saved settings. Start with a dry run: it reads Microsoft 365 and lists what would change, without changing anything."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:690
+msgid "Dry run"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:691
+msgid "Sync now"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:693
+msgid "Required application permissions (Microsoft Graph, admin consent): User.Read.All, and GroupMember.Read.All when groups are used."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:707
+msgid "You are not allowed to access this page."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:726
+msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:731
+msgid "Connected"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:731
+msgid "Setup incomplete"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:753
+msgid "Microsoft Entra ID app registration"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:754
+msgid "Enter the values from your app registration in the Microsoft Entra admin center."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:757
+msgid "Directory (tenant) ID"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:760
+msgid "Test tenant"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:762
+msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:764
+msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:770
+msgid "Application (client) ID"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:775
+msgid "How should WordPress authenticate to Microsoft?"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:780 includes/class-m365-login-admin.php:797
+msgid "Client secret"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:781
+msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:787
+msgid "Certificate"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:787
+msgid "Recommended"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:788
+msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:799
+msgid "•••••••••••• (stored, leave empty to keep)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:799
+msgid "Paste the secret value"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:800
+msgid "Show secret"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:805
+msgid "Remove the stored secret"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:808
+msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:812
+msgid "Step-by-step: create a client secret in Entra ID"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:815
+msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:816
+msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:817
+msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:818
+msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:819
msgid "Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need."
msgstr ""
-#: includes/class-m365-login-admin.php:477
+#: includes/class-m365-login-admin.php:820
msgid "Paste the value into the Client secret field above and save this page."
msgstr ""
-#: includes/class-m365-login-admin.php:479
+#: includes/class-m365-login-admin.php:822
msgid "When the secret expires, sign-ins fail with \"Could not complete the sign-in with Microsoft\". Create a new secret, paste it here, save, then delete the old one in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:492
+#: includes/class-m365-login-admin.php:835
msgid "Expired"
msgstr ""
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:496
+#: includes/class-m365-login-admin.php:839
msgid "Expires in %d days"
msgstr ""
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:499
+#: includes/class-m365-login-admin.php:842
msgid "Valid"
msgstr ""
-#: includes/class-m365-login-admin.php:504
+#: includes/class-m365-login-admin.php:847
msgid "Thumbprint (SHA-1)"
msgstr ""
-#: includes/class-m365-login-admin.php:506
+#: includes/class-m365-login-admin.php:849
msgid "Subject"
msgstr ""
-#: includes/class-m365-login-admin.php:508
+#: includes/class-m365-login-admin.php:851
msgid "Key size"
msgstr ""
-#: includes/class-m365-login-admin.php:510
+#: includes/class-m365-login-admin.php:853
msgid "Valid until"
msgstr ""
-#: includes/class-m365-login-admin.php:514
+#: includes/class-m365-login-admin.php:857
msgid "Download certificate (.cer)"
msgstr ""
-#: includes/class-m365-login-admin.php:515
+#: includes/class-m365-login-admin.php:858
msgid "Generate new certificate"
msgstr ""
-#: includes/class-m365-login-admin.php:518
+#: includes/class-m365-login-admin.php:861
msgid "Remove certificate when saving"
msgstr ""
-#: includes/class-m365-login-admin.php:522
+#: includes/class-m365-login-admin.php:865
msgid "No certificate stored yet."
msgstr ""
-#: includes/class-m365-login-admin.php:524
+#: includes/class-m365-login-admin.php:867
msgid "Generate certificate"
msgstr ""
-#: includes/class-m365-login-admin.php:525
+#: includes/class-m365-login-admin.php:868
msgid "3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable."
msgstr ""
-#: includes/class-m365-login-admin.php:529
+#: includes/class-m365-login-admin.php:872
msgid "Use your own certificate instead (paste PEM)"
msgstr ""
-#: includes/class-m365-login-admin.php:532
+#: includes/class-m365-login-admin.php:875
msgid "Private key (PEM, unencrypted)"
msgstr ""
-#: includes/class-m365-login-admin.php:536
+#: includes/class-m365-login-admin.php:879
msgid "Certificate (PEM)"
msgstr ""
-#: includes/class-m365-login-admin.php:538
+#: includes/class-m365-login-admin.php:881
msgid "RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards."
msgstr ""
-#: includes/class-m365-login-admin.php:544
+#: includes/class-m365-login-admin.php:887
msgid "Step-by-step: register the certificate in Entra ID"
msgstr ""
-#: includes/class-m365-login-admin.php:547
+#: includes/class-m365-login-admin.php:890
msgid "Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part."
msgstr ""
-#: includes/class-m365-login-admin.php:548
+#: includes/class-m365-login-admin.php:891
msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app."
msgstr ""
-#: includes/class-m365-login-admin.php:549
+#: includes/class-m365-login-admin.php:892
msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate."
msgstr ""
-#: includes/class-m365-login-admin.php:550
+#: includes/class-m365-login-admin.php:893
msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add."
msgstr ""
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:894
msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly."
msgstr ""
-#: includes/class-m365-login-admin.php:552
+#: includes/class-m365-login-admin.php:895
msgid "Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now."
msgstr ""
-#: includes/class-m365-login-admin.php:554
+#: includes/class-m365-login-admin.php:897
msgid "How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted."
msgstr ""
-#: includes/class-m365-login-admin.php:555
+#: includes/class-m365-login-admin.php:898
msgid "Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch."
msgstr ""
-#: includes/class-m365-login-admin.php:561
+#: includes/class-m365-login-admin.php:904
msgid "Account prompt"
msgstr ""
-#: includes/class-m365-login-admin.php:563
+#: includes/class-m365-login-admin.php:906
msgid "Always let the user pick an account (recommended)"
msgstr ""
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:907
msgid "Use the current Microsoft session if available"
msgstr ""
-#: includes/class-m365-login-admin.php:565
+#: includes/class-m365-login-admin.php:908
msgid "Always require re-entering credentials"
msgstr ""
-#: includes/class-m365-login-admin.php:574
+#: includes/class-m365-login-admin.php:917
msgid "Appearance"
msgstr ""
-#: includes/class-m365-login-admin.php:577
+#: includes/class-m365-login-admin.php:920
msgid "Live preview"
msgstr ""
-#: includes/class-m365-login-admin.php:591
+#: includes/class-m365-login-admin.php:934
msgid "Button text"
msgstr ""
-#: includes/class-m365-login-admin.php:595
+#: includes/class-m365-login-admin.php:938
msgid "Divider text"
msgstr ""
-#: includes/class-m365-login-admin.php:597
+#: includes/class-m365-login-admin.php:940
msgid "Leave empty to hide the divider line."
msgstr ""
-#: includes/class-m365-login-admin.php:602
+#: includes/class-m365-login-admin.php:945
msgid "Icon"
msgstr ""
-#: includes/class-m365-login-admin.php:605
+#: includes/class-m365-login-admin.php:948
msgid "Show an icon on the button"
msgstr ""
-#: includes/class-m365-login-admin.php:616
+#: includes/class-m365-login-admin.php:959
msgid "Default: Microsoft logo"
msgstr ""
-#: includes/class-m365-login-admin.php:618
+#: includes/class-m365-login-admin.php:961
msgid "Choose from media library"
msgstr ""
-#: includes/class-m365-login-admin.php:619
+#: includes/class-m365-login-admin.php:962
msgid "Use Microsoft logo"
msgstr ""
-#: includes/class-m365-login-admin.php:621
+#: includes/class-m365-login-admin.php:964
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr ""
-#: includes/class-m365-login-admin.php:629
+#: includes/class-m365-login-admin.php:972
msgid "Background"
msgstr ""
-#: includes/class-m365-login-admin.php:630
+#: includes/class-m365-login-admin.php:973
msgid "Background (hover)"
msgstr ""
-#: includes/class-m365-login-admin.php:631
+#: includes/class-m365-login-admin.php:974
msgid "Text colour"
msgstr ""
-#: includes/class-m365-login-admin.php:632
+#: includes/class-m365-login-admin.php:975
msgid "Border"
msgstr ""
-#: includes/class-m365-login-admin.php:645
+#: includes/class-m365-login-admin.php:988
msgid "Corner radius"
msgstr ""
-#: includes/class-m365-login-admin.php:649
+#: includes/class-m365-login-admin.php:992
msgid "Position on the login page"
msgstr ""
-#: includes/class-m365-login-admin.php:651
+#: includes/class-m365-login-admin.php:994
msgid "Below the login form"
msgstr ""
-#: includes/class-m365-login-admin.php:652
+#: includes/class-m365-login-admin.php:995
msgid "Above the login form"
msgstr ""
-#: includes/class-m365-login-admin.php:658
+#: includes/class-m365-login-admin.php:1001
msgid "Quick presets"
msgstr ""
-#: includes/class-m365-login-admin.php:659
+#: includes/class-m365-login-admin.php:1002
msgid "Microsoft dark"
msgstr ""
-#: includes/class-m365-login-admin.php:660
+#: includes/class-m365-login-admin.php:1003
msgid "Microsoft light"
msgstr ""
-#: includes/class-m365-login-admin.php:661
+#: includes/class-m365-login-admin.php:1004
msgid "Azure blue"
msgstr ""
-#: includes/class-m365-login-admin.php:662
+#: includes/class-m365-login-admin.php:1005
msgid "WordPress blue"
msgstr ""
-#: includes/class-m365-login-admin.php:666
+#: includes/class-m365-login-admin.php:1009
msgid "Custom login page"
msgstr ""
-#: includes/class-m365-login-admin.php:667
+#: includes/class-m365-login-admin.php:1010
msgid "Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there."
msgstr ""
-#: includes/class-m365-login-admin.php:670
+#: includes/class-m365-login-admin.php:1013
msgid "URL of your login page"
msgstr ""
-#: includes/class-m365-login-admin.php:672
+#: includes/class-m365-login-admin.php:1015
msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr ""
-#: includes/class-m365-login-admin.php:678
+#: includes/class-m365-login-admin.php:1021
msgid "Add the button to every wp_login_form() form automatically"
msgstr ""
-#: includes/class-m365-login-admin.php:679
+#: includes/class-m365-login-admin.php:1022
msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr ""
-#: includes/class-m365-login-admin.php:684
+#: includes/class-m365-login-admin.php:1027
msgid "Manual placement"
msgstr ""
-#: includes/class-m365-login-admin.php:685
+#: includes/class-m365-login-admin.php:1028
msgid "Shortcode (block editor, page builders):"
msgstr ""
-#: includes/class-m365-login-admin.php:687
+#: includes/class-m365-login-admin.php:1030
msgid "Template function (theme files):"
msgstr ""
-#: includes/class-m365-login-admin.php:689
+#: includes/class-m365-login-admin.php:1032
msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
msgstr ""
-#: includes/class-m365-login-admin.php:697
+#: includes/class-m365-login-admin.php:1040
msgid "User matching & hardening"
msgstr ""
-#: includes/class-m365-login-admin.php:698
-msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
+#: includes/class-m365-login-admin.php:1041
+msgid "Sign-in never creates users. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists – created by hand or imported by the user sync."
msgstr ""
-#: includes/class-m365-login-admin.php:703
+#: includes/class-m365-login-admin.php:1046
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr ""
-#: includes/class-m365-login-admin.php:704
+#: includes/class-m365-login-admin.php:1047
msgid "On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended."
msgstr ""
-#: includes/class-m365-login-admin.php:711
+#: includes/class-m365-login-admin.php:1054
msgid "Fall back to the user principal name (UPN)"
msgstr ""
-#: includes/class-m365-login-admin.php:712
+#: includes/class-m365-login-admin.php:1055
msgid "If the token contains no \"email\" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts."
msgstr ""
-#: includes/class-m365-login-admin.php:719
+#: includes/class-m365-login-admin.php:1062
msgid "Keep users signed in (\"Remember me\")"
msgstr ""
-#: includes/class-m365-login-admin.php:720
+#: includes/class-m365-login-admin.php:1063
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr ""
-#: includes/class-m365-login-admin.php:725
+#: includes/class-m365-login-admin.php:1068
msgid "Allowed e-mail domains (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:727
+#: includes/class-m365-login-admin.php:1070
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr ""
-#: includes/class-m365-login-admin.php:732
+#: includes/class-m365-login-admin.php:1075
msgid "Allowed Entra groups (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:733
+#: includes/class-m365-login-admin.php:1076
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr ""
-#: includes/class-m365-login-admin.php:736
-msgid "Search groups"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:738
-msgid "Type a group name or paste an object ID…"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:739
-msgid "Search"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:744
-msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:750
-msgid "Selected groups"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:751
+#: includes/class-m365-login-admin.php:1078
msgid "No groups selected – every matched user may sign in."
msgstr ""
-#: includes/class-m365-login-admin.php:761
+#: includes/class-m365-login-admin.php:1080
msgid "Membership is read from the \"groups\" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission \"User.Read.All\" or \"Directory.Read.All\"). If neither works, the sign-in is refused."
msgstr ""
-#: includes/class-m365-login-admin.php:766
+#: includes/class-m365-login-admin.php:1085
msgid "Button-only mode"
msgstr ""
-#: includes/class-m365-login-admin.php:767
+#: includes/class-m365-login-admin.php:1086
msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected."
msgstr ""
-#: includes/class-m365-login-admin.php:772
+#: includes/class-m365-login-admin.php:1091
msgid "Show only the Microsoft button on the login page"
msgstr ""
-#: includes/class-m365-login-admin.php:773
+#: includes/class-m365-login-admin.php:1092
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr ""
-#: includes/class-m365-login-admin.php:778
+#: includes/class-m365-login-admin.php:1097
msgid "Fallback link (keep it secret)"
msgstr ""
-#: includes/class-m365-login-admin.php:779
+#: includes/class-m365-login-admin.php:1098
msgid "Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
msgstr ""
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:1106
msgid "Generate a new key when saving"
msgstr ""
-#: includes/class-m365-login-admin.php:790
+#: includes/class-m365-login-admin.php:1109
msgid "A key is generated automatically the first time you save these settings."
msgstr ""
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:796
+#: includes/class-m365-login-admin.php:1115
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr ""
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:805
+#: includes/class-m365-login-admin.php:1124
msgid "What the plugin does to keep sign-ins safe"
msgstr ""
-#: includes/class-m365-login-admin.php:807
+#: includes/class-m365-login-admin.php:1126
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr ""
-#: includes/class-m365-login-admin.php:808
+#: includes/class-m365-login-admin.php:1127
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr ""
-#: includes/class-m365-login-admin.php:809
+#: includes/class-m365-login-admin.php:1128
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr ""
-#: includes/class-m365-login-admin.php:810
-msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
+#: includes/class-m365-login-admin.php:1129
+msgid "Client secret encrypted at rest; sign-in never creates accounts or changes passwords."
msgstr ""
-#: includes/class-m365-login-admin.php:816
+#: includes/class-m365-login-admin.php:1137
msgid "Save changes"
msgstr ""
-#: includes/class-m365-login-admin.php:822
+#: includes/class-m365-login-admin.php:1143
msgid "Redirect URI"
msgstr ""
-#: includes/class-m365-login-admin.php:823
+#: includes/class-m365-login-admin.php:1144
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr ""
-#: includes/class-m365-login-admin.php:829
+#: includes/class-m365-login-admin.php:1150
msgid "Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:832
+#: includes/class-m365-login-admin.php:1153
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr ""
-#: includes/class-m365-login-admin.php:837
+#: includes/class-m365-login-admin.php:1158
msgid "Setup guide: app registration"
msgstr ""
-#: includes/class-m365-login-admin.php:839
+#: includes/class-m365-login-admin.php:1160
msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration."
msgstr ""
-#: includes/class-m365-login-admin.php:840
+#: includes/class-m365-login-admin.php:1161
msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)."
msgstr ""
-#: includes/class-m365-login-admin.php:841
+#: includes/class-m365-login-admin.php:1162
msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register."
msgstr ""
-#: includes/class-m365-login-admin.php:842
+#: includes/class-m365-login-admin.php:1163
msgid "On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab."
msgstr ""
-#: includes/class-m365-login-admin.php:843
+#: includes/class-m365-login-admin.php:1164
msgid "Authentication: leave \"ID tokens\" unchecked (the plugin uses the authorization code flow) and \"Allow public client flows\" on No."
msgstr ""
-#: includes/class-m365-login-admin.php:844
+#: includes/class-m365-login-admin.php:1165
msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt."
msgstr ""
-#: includes/class-m365-login-admin.php:845
+#: includes/class-m365-login-admin.php:1166
msgid "Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate)."
msgstr ""
-#: includes/class-m365-login-admin.php:846
+#: includes/class-m365-login-admin.php:1167
msgid "Optional: restrict who may use the app under Enterprise applications → your app → Properties → \"Assignment required\" = Yes, then assign users/groups."
msgstr ""
-#: includes/class-m365-login-admin.php:848
+#: includes/class-m365-login-admin.php:1169
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr ""
-#: includes/class-m365-login-admin.php:849
-msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
+#: includes/class-m365-login-admin.php:1170
+msgid "Optional, for group restrictions and the user sync: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr ""
-#: includes/class-m365-login-admin.php:853
+#: includes/class-m365-login-admin.php:1174
msgid "Shortcode"
msgstr ""
-#: includes/class-m365-login-admin.php:854
+#: includes/class-m365-login-admin.php:1175
msgid "Place the button on a custom login page:"
msgstr ""
-#: includes/class-m365-login-admin.php:856
+#: includes/class-m365-login-admin.php:1177
msgid "More options on the Button tab under \"Custom login page\"."
msgstr ""
@@ -724,74 +1049,78 @@ msgstr ""
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr ""
-#: includes/class-m365-login-auth.php:823
+#: includes/class-m365-login-auth.php:827
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr ""
-#: includes/class-m365-login-auth.php:844 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:848 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr ""
-#: includes/class-m365-login-auth.php:845
+#: includes/class-m365-login-auth.php:849
msgid "The login request expired or was invalid. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:846
+#: includes/class-m365-login-auth.php:850
msgid "Microsoft sign-in was cancelled."
msgstr ""
-#: includes/class-m365-login-auth.php:847
+#: includes/class-m365-login-auth.php:851
msgid "Microsoft returned an error. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:848
+#: includes/class-m365-login-auth.php:852
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:849
+#: includes/class-m365-login-auth.php:853
msgid "The Microsoft sign-in could not be verified."
msgstr ""
-#: includes/class-m365-login-auth.php:850
+#: includes/class-m365-login-auth.php:854
msgid "Your Microsoft account did not provide an e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:851
+#: includes/class-m365-login-auth.php:855
msgid "Your e-mail domain is not allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:852
+#: includes/class-m365-login-auth.php:856
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:853
+#: includes/class-m365-login-auth.php:857
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:854
+#: includes/class-m365-login-auth.php:858
msgid "You are not allowed to sign in with this account."
msgstr ""
-#: includes/class-m365-login-auth.php:855
+#: includes/class-m365-login-auth.php:859
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:856
+#: includes/class-m365-login-auth.php:860
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:857
+#: includes/class-m365-login-auth.php:861
msgid "The fallback key is not valid."
msgstr ""
-#: includes/class-m365-login-auth.php:858
+#: includes/class-m365-login-auth.php:862
msgid "Too many attempts. Please wait 15 minutes."
msgstr ""
-#: includes/class-m365-login-auth.php:859
+#: includes/class-m365-login-auth.php:863
msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again."
msgstr ""
+#: includes/class-m365-login-auth.php:864 includes/class-m365-login-sync.php:1404
+msgid "This account has been deactivated."
+msgstr ""
+
#: includes/class-m365-login-certificate.php:29
msgid "The PHP OpenSSL extension is not available."
msgstr ""
@@ -844,75 +1173,369 @@ msgstr ""
msgid "The certificate has already expired."
msgstr ""
-#: includes/class-m365-login-graph.php:201
+#: includes/class-m365-login-graph.php:375
msgid "Group"
msgstr ""
-#: includes/class-m365-login-graph.php:203
+#: includes/class-m365-login-graph.php:377
msgid "Security group"
msgstr ""
-#: includes/class-m365-login-graph.php:205
+#: includes/class-m365-login-graph.php:379
msgid "Microsoft 365 group"
msgstr ""
-#: includes/class-m365-login-settings.php:47
+#: includes/class-m365-login-settings.php:54
msgid "Sign in with Microsoft"
msgstr ""
-#: includes/class-m365-login-settings.php:56
+#: includes/class-m365-login-settings.php:63
msgid "or"
msgstr ""
-#: includes/class-m365-login-settings.php:197 includes/class-m365-login-settings.php:445
+#: includes/class-m365-login-settings.php:225 includes/class-m365-login-settings.php:530
msgid "The private key could not be encrypted. Is the OpenSSL extension available?"
msgstr ""
-#: includes/class-m365-login-settings.php:391
+#: includes/class-m365-login-settings.php:476
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr ""
-#: includes/class-m365-login-settings.php:399
+#: includes/class-m365-login-settings.php:484
msgid "The application (client) ID must be a GUID."
msgstr ""
-#: includes/class-m365-login-settings.php:411
+#: includes/class-m365-login-settings.php:496
msgid "The client secret contains invalid characters."
msgstr ""
-#: includes/class-m365-login-settings.php:415
+#: includes/class-m365-login-settings.php:500
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr ""
-#: includes/class-m365-login-settings.php:435
+#: includes/class-m365-login-settings.php:520
msgid "Please paste both the private key and the certificate."
msgstr ""
-#: includes/class-m365-login-settings.php:437
+#: includes/class-m365-login-settings.php:522
msgid "The pasted key or certificate is too large."
msgstr ""
-#: includes/class-m365-login-settings.php:454
+#: includes/class-m365-login-settings.php:539
msgid "Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then."
msgstr ""
-#: includes/class-m365-login-settings.php:500
+#: includes/class-m365-login-settings.php:571
msgid "The custom login page must be a URL on this site."
msgstr ""
-#: includes/class-m365-login.php:102
+#: includes/class-m365-login-settings.php:678
+msgid "User sync: \"Delete\" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:109
+msgid "Display name"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:113
+msgid "First name"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:117
+msgid "Last name"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:121
+msgid "Profile photo (used as avatar)"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:125
+msgid "Job title"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:129
+msgid "Department"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:133
+msgid "Company"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:137
+msgid "Office"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:141
+msgid "Employee ID"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:145
+msgid "Business phone"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:149
+msgid "Mobile phone"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:153
+msgid "Street address"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:157
+msgid "Postal code"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:161
+msgid "City"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:165
+msgid "State / province"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:169
+msgid "Country"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:173
+msgid "Language (sets the admin language if installed)"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:308
+msgid "Another sync is still running. Please try again in a few minutes."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:365
+msgid "The connection to Microsoft Entra ID is not configured yet."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:369
+msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:373
+msgid "The default role does not exist. Please check the sync settings."
+msgstr ""
+
+#. translators: %d: number of users
+#: includes/class-m365-login-sync.php:385
+msgid "%d user read from Microsoft 365."
+msgid_plural "%d users read from Microsoft 365."
+msgstr[0] ""
+msgstr[1] ""
+
+#. translators: 1: number of accounts, 2: limit
+#: includes/class-m365-login-sync.php:435
+msgid "Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter)."
+msgstr ""
+
+#. translators: %s: user principal name
+#: includes/class-m365-login-sync.php:559
+msgid "%s: no usable e-mail address, skipped."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:566
+msgid "%s: e-mail domain is not on the allow-list, skipped."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:578
+msgid "%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:582
+msgid "%s: existing account linked."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:596 includes/class-m365-login-sync.php:931 includes/class-m365-login-sync.php:1536
+msgid "disabled in Microsoft 365"
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:607
+msgid "%s: added to this site."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:616
+msgid "%s: reactivated (active in Microsoft 365 again)."
+msgstr ""
+
+#. translators: 1: e-mail address, 2: list of changed fields
+#: includes/class-m365-login-sync.php:627
+msgid "%1$s: updated (%2$s)."
+msgstr ""
+
+#. translators: 1: e-mail address, 2: role names
+#: includes/class-m365-login-sync.php:653
+msgid "%1$s: account created (%2$s)."
+msgstr ""
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:680
+msgid "%1$s: account could not be created: %2$s"
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:738
+msgid "%s: e-mail address is used by another WordPress account and was not changed."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:741
+msgid "e-mail"
+msgstr ""
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:785
+msgid "%1$s: profile could not be updated: %2$s"
+msgstr ""
+
+#. translators: %s: role names
+#: includes/class-m365-login-sync.php:891
+msgid "roles: %s"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:926 includes/class-m365-login-sync.php:1537
+msgid "deleted in Microsoft 365"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:934 includes/class-m365-login-sync.php:1538
+msgid "no longer a member of the sync groups"
+msgstr ""
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:954
+msgid "%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:987
+msgid "%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted."
+msgstr ""
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:996
+msgid "%1$s: account deleted (%2$s)."
+msgstr ""
+
+#. translators: 1: e-mail address, 2: reason
+#: includes/class-m365-login-sync.php:1006
+msgid "%1$s: account deactivated (%2$s)."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1085
+msgid "Microsoft Graph refused the request. Grant the application permissions \"User.Read.All\" and \"GroupMember.Read.All\" with admin consent in Entra ID."
+msgstr ""
+
+#. translators: %s: error message
+#: includes/class-m365-login-sync.php:1088
+msgid "Microsoft Graph error: %s"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1106
+msgid "Log truncated."
+msgstr ""
+
+#. translators: 1: e-mail address, 2: error message
+#: includes/class-m365-login-sync.php:1168
+msgid "%1$s: profile photo could not be read: %2$s"
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1176
+msgid "%s: profile photo removed."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1192
+msgid "%s: profile photo could not be downloaded."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1199
+msgid "%s: profile photo is not a valid image or could not be saved."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1216
+msgid "%s: profile photo updated."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1433 includes/class-m365-login-sync.php:1566
+msgid "Microsoft 365"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1451
+msgid "Deactivated"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1454
+msgid "Imported"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1456
+msgid "Linked"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1484
+msgid "Reactivate"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1484
+msgid "Deactivate"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1517
+msgid "The account has been deactivated and signed out everywhere."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1518
+msgid "The account has been reactivated."
+msgstr ""
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1542
+msgid "Deactivated since %1$s (%2$s)"
+msgstr ""
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1544
+msgid "manually"
+msgstr ""
+
+#. translators: 1: date, 2: reason
+#: includes/class-m365-login-sync.php:1546
+msgid "Status"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1549
+msgid "Object ID"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1553
+msgid "Last sync"
+msgstr ""
+
+#: includes/class-m365-login-sync.php:1575
+msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
+msgstr ""
+
+#: includes/class-m365-login.php:110
msgid "Settings"
msgstr ""
-#: includes/class-m365-login.php:113
+#: includes/class-m365-login.php:121
msgid "M365 Login requires PHP 7.4 or newer."
msgstr ""
-#: includes/class-m365-login.php:114 includes/class-m365-login.php:123
+#: includes/class-m365-login.php:122 includes/class-m365-login.php:131
msgid "Plugin activation failed"
msgstr ""
-#: includes/class-m365-login.php:122
+#: includes/class-m365-login.php:130
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr ""
diff --git a/m365-login.php b/m365-login.php
index 3729395..5915bd5 100644
--- a/m365-login.php
+++ b/m365-login.php
@@ -2,8 +2,8 @@
/**
* Plugin Name: M365 Login
* Plugin URI: https://github.com/friloo/wp-m365-login
- * Description: Adds a customisable "Sign in with Microsoft" button to the WordPress login page. Existing users are matched by e-mail address via Microsoft Entra ID (OpenID Connect, PKCE).
- * Version: 1.0.0
+ * Description: Adds a customisable "Sign in with Microsoft" button to the WordPress login page. Users are matched by e-mail address via Microsoft Entra ID (OpenID Connect, PKCE); an optional user sync imports Microsoft 365 users with roles and profile fields.
+ * Version: 1.1.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: friloo
@@ -16,7 +16,7 @@
defined( 'ABSPATH' ) || exit;
-define( 'M365_LOGIN_VERSION', '1.0.0' );
+define( 'M365_LOGIN_VERSION', '1.1.0' );
define( 'M365_LOGIN_FILE', __FILE__ );
define( 'M365_LOGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'M365_LOGIN_URL', plugin_dir_url( __FILE__ ) );
@@ -28,11 +28,13 @@ require_once M365_LOGIN_DIR . 'includes/class-m365-login-jwt.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-certificate.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-graph.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-auth.php';
+require_once M365_LOGIN_DIR . 'includes/class-m365-login-sync.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-button.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-admin.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login.php';
require_once M365_LOGIN_DIR . 'includes/functions.php';
register_activation_hook( __FILE__, array( 'M365_Login', 'activate' ) );
+register_deactivation_hook( __FILE__, array( 'M365_Login_Sync', 'unschedule' ) );
add_action( 'plugins_loaded', array( 'M365_Login', 'instance' ) );
diff --git a/readme.txt b/readme.txt
index 138766a..5f6b820 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: microsoft, entra id, azure ad, sso, login
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 7.4
-Stable tag: 1.0.0
+Stable tag: 1.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -16,7 +16,8 @@ Adds a customisable "Sign in with Microsoft" button to the login page. Existing
The plugin is deliberately small and strict:
-* **No user provisioning.** A Microsoft sign-in succeeds only when a WordPress user with the same e-mail address already exists. Nobody gets an account just by having a Microsoft login.
+* **Sign-in never creates users.** A Microsoft sign-in succeeds only when a WordPress user with the same e-mail address already exists. Nobody gets an account just by having a Microsoft login.
+* **Optional user sync.** Import all Microsoft 365 users (or the members of selected groups) as WordPress accounts, assign a default role plus extra roles through a group → role mapping, copy selected profile attributes and the profile photo, and deactivate or delete WordPress accounts whose Microsoft 365 account was disabled or deleted. Runs on demand, on a WP-Cron schedule or with `wp m365-login sync`; a dry run shows every change first.
* **Password login stays available.** The button is an additional option; the normal form is untouched.
* **Fully customisable button.** Change the text, replace the Microsoft logo with your own icon from the media library, pick background, hover, text and border colours, adjust the corner radius, and choose whether the button appears above or below the login form – with a live preview.
* **Entra group restriction.** Search and pick the groups whose members may sign in, right in the settings screen. Membership is checked via the ID token's `groups` claim or Microsoft Graph (nested groups included).
@@ -37,6 +38,8 @@ The plugin is deliberately small and strict:
* The **client secret / private key is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again.
* In multi-tenant mode the unverified `email` claim is ignored; matching uses the user principal name (verified domain) only.
* Login starts and fallback-key attempts are rate limited per client.
+* The user sync stops before deactivating anything when a Microsoft Graph request fails, treats an account as deleted only when Graph returns 404 for its object ID, refuses to deactivate or delete more than 20 % of the linked accounts in one run, and never touches administrators that existed before the sync or your own account.
+* Deactivated accounts lose every sign-in path (Microsoft, password, application passwords) and all sessions immediately.
* Every setting is sanitised, every output escaped, every admin request nonce- and capability-checked.
= Developer hooks =
@@ -47,10 +50,12 @@ The plugin is deliberately small and strict:
* `m365_login_allow_user` – filter, return `false` to block a matched user (e.g. group checks).
* `m365_login_success` – action after a successful sign-in, receives the user and verified claims.
* `m365_login_block_password_login` – filter, return `false` to exempt a password sign-in from button-only mode.
+* `m365_login_sync_attributes`, `m365_login_sync_roles`, `m365_login_sync_new_user_data`, `m365_login_sync_email`, `m365_login_sync_protect_user`, `m365_login_sync_deprovision_limit`, `m365_login_sync_photo_limit`, `m365_login_sync_photo_interval` – filters for the user sync.
+* `m365_login_sync_user_created`, `m365_login_sync_finished`, `m365_login_user_disabled`, `m365_login_user_enabled` – actions for the user sync.
== External services ==
-This plugin connects to **Microsoft identity platform (Microsoft Entra ID)** to authenticate users. It is required for the plugin's only purpose – signing users in with their Microsoft account – and is only contacted when a user clicks the Microsoft button or when an administrator uses the "Test tenant" button.
+This plugin connects to **Microsoft identity platform (Microsoft Entra ID)** to authenticate users. It is required for the plugin's main purpose – signing users in with their Microsoft account – and is only contacted when a user clicks the Microsoft button, when an administrator uses the "Test tenant" button, or when the optional user sync runs.
Endpoints used (all under `https://login.microsoftonline.com/`):
@@ -59,12 +64,14 @@ Endpoints used (all under `https://login.microsoftonline.com/`):
* `/{tenant}/discovery/v2.0/keys` – the server downloads Microsoft's public signing keys to verify the ID token. No user data is sent.
* `/{tenant}/v2.0/.well-known/openid-configuration` – fetched only when an administrator clicks "Test tenant". No user data is sent.
-When the optional **group restriction** is configured, the plugin additionally connects to **Microsoft Graph** (`https://graph.microsoft.com/v1.0/`) using an application token obtained from `/{tenant}/oauth2/v2.0/token` (client credentials, client ID and secret are sent):
+When the optional **group restriction** or the optional **user sync** is used, the plugin additionally connects to **Microsoft Graph** (`https://graph.microsoft.com/v1.0/`) using an application token obtained from `/{tenant}/oauth2/v2.0/token` (client credentials, client ID and secret or signed assertion are sent):
* `/groups` – only when an administrator searches for groups in the settings screen. The typed search text is sent.
* `/users/{id}/checkMemberGroups` – during sign-in when the ID token carries no usable `groups` claim. The user's Microsoft object ID and the configured group IDs are sent; Microsoft returns which of those groups the user belongs to.
+* `/users`, `/groups/{id}/transitiveMembers`, `/users/{id}` – only while the user sync runs (manually, on the configured schedule or via WP-CLI). The configured group IDs and the object IDs of linked accounts are sent; Microsoft returns the users with their account status and the profile attributes selected in the settings.
+* `/users/{id}/photos/240x240`, `/users/{id}/photo` – only while the user sync runs and "Profile photo" is selected. Returns the user's profile photo.
-The plugin receives the user's e-mail address / user principal name, display name and Microsoft object ID from Microsoft and uses them solely to find the matching WordPress account. Nothing else is stored.
+For sign-in the plugin receives the user's e-mail address / user principal name, display name and Microsoft object ID and uses them solely to find the matching WordPress account. The user sync stores the object ID, the account status and the attributes selected by the administrator (for example name, job title, department, phone numbers, profile photo) in the WordPress user profile; profile photos are saved in `wp-content/uploads/m365-login-avatars/` and are shown publicly wherever WordPress displays avatars.
Microsoft terms and privacy: [Microsoft Services Agreement](https://www.microsoft.com/servicesagreement), [Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement), [Microsoft identity platform documentation](https://learn.microsoft.com/entra/identity-platform/).
@@ -88,7 +95,15 @@ Both work. A certificate is recommended: the private key stays on your server (e
= Does the plugin create users? =
-No. Users must already exist in WordPress. The e-mail address is the only link between the Microsoft account and the WordPress account. This is intentional – it keeps the administrator in control of who can access the site.
+Signing in never creates users: they must already exist in WordPress, linked by e-mail address. If you want accounts for your Microsoft 365 users, enable the **User sync** tab: it imports all users (or the members of selected groups) ahead of time, assigns roles and keeps profiles up to date.
+
+= What happens to people who leave the organisation? =
+
+With the user sync, a WordPress account whose Microsoft 365 account is disabled or deleted can be deactivated (no sign-in of any kind, sessions ended) or deleted (content reassigned to a user you pick). If the Microsoft 365 account is enabled again, an account deactivated by the sync is reactivated automatically. Administrators that existed before the sync are never deactivated or deleted automatically.
+
+= Which Microsoft Graph permissions does the user sync need? =
+
+The application permission `User.Read.All` with admin consent, plus `GroupMember.Read.All` when you limit the sync to groups or map groups to roles. A tenant GUID must be configured on the Connection tab.
= Which accounts can sign in? =
@@ -120,7 +135,7 @@ Yes. Settings are per site; a user must be a member of the site (or a super admi
= What happens on uninstall? =
-The settings, cached data and the per-user Microsoft object ID are removed.
+The settings, cached data, the sync report and schedule, stored profile photos and the per-user plugin data (Microsoft object ID, deactivation status) are removed. Imported accounts and copied profile fields (`m365_*` user meta) are kept. Deactivated accounts become active again, so delete them first if they must stay locked.
== Screenshots ==
@@ -132,10 +147,20 @@ The settings, cached data and the per-user Microsoft object ID are removed.
== Changelog ==
+= 1.1.0 =
+* New: user sync – import Microsoft 365 users (whole tenant or selected groups) with a default role and group → role mapping, selectable profile attributes and profile photos as avatars.
+* New: deactivate or delete WordPress accounts whose Microsoft 365 account was disabled or deleted; automatic reactivation; dry run, safety stop and protected administrators.
+* New: "Microsoft 365" column, deactivate/reactivate row actions and a read-only Microsoft 365 section on the profile screen.
+* New: `wp m365-login sync [--dry-run]` WP-CLI command and scheduled sync via WP-Cron.
+* Fix: generating or removing the certificate in the settings did not keep the change and broke a stored client secret.
+
= 1.0.0 =
* Initial release.
== Upgrade Notice ==
+= 1.1.0 =
+Adds an optional Microsoft 365 user sync (import, roles, profile fields, deactivation). Nothing changes until you enable it on the new User sync tab.
+
= 1.0.0 =
Initial release.
diff --git a/uninstall.php b/uninstall.php
index 1a2ecec..f0ad67c 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -18,6 +18,18 @@ function m365_login_uninstall_site() {
global $wpdb;
delete_option( 'm365_login_settings' );
+ delete_option( 'm365_login_sync_report' );
+ wp_clear_scheduled_hook( 'm365_login_sync' );
+
+ // Synced profile photos (uploads/m365-login-avatars/).
+ $uploads = wp_get_upload_dir();
+ $dir = trailingslashit( $uploads['basedir'] ) . 'm365-login-avatars';
+ if ( is_dir( $dir ) ) {
+ foreach ( (array) glob( $dir . '/m365-*' ) as $file ) {
+ wp_delete_file( $file );
+ }
+ @rmdir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- best effort, may contain foreign files.
+ }
// Transients: state records and JWKS cache.
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
@@ -40,6 +52,7 @@ if ( is_multisite() ) {
m365_login_uninstall_site();
}
-// User meta is global.
-delete_metadata( 'user', 0, '_m365_login_oid', '', true );
-delete_metadata( 'user', 0, '_m365_login_last_login', '', true );
+// User meta is global. Imported accounts stay; copied profile fields (m365_*) are kept as ordinary user data.
+foreach ( array( '_m365_login_oid', '_m365_login_last_login', '_m365_login_synced', '_m365_login_disabled', '_m365_login_last_sync', '_m365_login_photo' ) as $m365_login_meta_key ) {
+ delete_metadata( 'user', 0, $m365_login_meta_key, '', true );
+}