diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99887a3..24864d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ All notable changes to this project are documented in this file. The format foll
- User sync (new "User sync" tab): imports Microsoft 365 / Entra ID users as WordPress accounts – the whole tenant or the (nested) members of selected groups, guests optional – and links existing accounts by e-mail address.
- Default role plus group → role mapping with a Graph-powered group picker; mapped roles either added to or replacing the default role (first match wins, reorderable). Roles of accounts that existed before the sync are only managed on request.
- Selectable profile attributes (display name, first/last name, job title, department, company, office, employee ID, phones, address, language) and the profile photo, which replaces the Gravatar.
+- Profile photos follow Microsoft 365 on every run: versions are compared via Graph `$batch` (20 users per request), changed photos are downloaded again (old file deleted, new URL), photos deleted in Microsoft 365 are deleted in WordPress. Errors never delete a photo.
+- Deselected profile fields (`m365_*`) and photos are removed from the profiles on the next run; fields cleared in Microsoft 365 are cleared in WordPress; a user's photo is deleted together with the user.
- Deactivation or deletion of WordPress accounts whose Microsoft 365 account was disabled, deleted or removed from the sync groups; automatic reactivation. Deactivated accounts cannot sign in at all (Microsoft, password, application passwords) and lose all sessions.
- Safeguards: dry run, safety stop above 20 % deprovisioning (at least 5 accounts), abort on any Graph error, deletion only on a 404 for the object ID, protected pre-existing administrators and own account, content reassignment required for deletion, run lock.
- Scheduled sync via WP-Cron (hourly, twice daily, daily), `wp m365-login sync [--dry-run]`, report of the last run in the settings.
diff --git a/README.md b/README.md
index f0958d1..ee0f185 100644
--- a/README.md
+++ b/README.md
@@ -319,7 +319,22 @@ vollständig – manuelle Änderungen werden überschrieben.
Telefon (geschäftlich/mobil), Adresse, Sprache. Namen landen in den normalen WordPress-Feldern, alles andere in User-Meta mit
dem Präfix `m365_` (z. B. `m365_department`) und wird auf der Profilseite angezeigt. Das **Profilbild** wird nach
`wp-content/uploads/m365-login-avatars/` geladen (Dateiname mit gesalzenem Hash statt Objekt-ID) und ersetzt überall den
-Gravatar; es wird etwa einmal täglich pro Benutzer geprüft. Achtung: Avatare sind öffentlich sichtbar, wo WordPress sie anzeigt.
+Gravatar. Achtung: Avatare sind öffentlich sichtbar, wo WordPress sie anzeigt.
+
+Microsoft 365 hat immer Vorrang, bei jedem Lauf:
+
+| In Microsoft 365 … | … in WordPress |
+| --- | --- |
+| Feld geändert | Feld wird überschrieben. |
+| Feld geleert | Feld wird geleert (`m365_*`-Meta gelöscht; ein leerer Anzeigename wird nicht übernommen). |
+| Profilbild geändert | Neues Bild wird geladen, das alte gelöscht; die Avatar-URL ändert sich, damit Browser nicht das alte Bild zeigen. |
+| Profilbild gelöscht | Bild wird gelöscht, der Avatar fällt auf Gravatar zurück. |
+
+Die Bildversionen werden per Graph-`$batch` geprüft (20 Benutzer pro Anfrage); heruntergeladen werden nur geänderte Bilder,
+höchstens 500 pro Lauf, der Rest folgt im nächsten (Filter `m365_login_sync_photo_limit`, Prüfintervall per
+`m365_login_sync_photo_interval`). Bei einem Fehler von Microsoft bleibt das vorhandene Bild erhalten – gelöscht wird nur,
+wenn Microsoft ausdrücklich „kein Foto“ meldet. **Abgewählte** Felder und Bilder werden beim nächsten Lauf aus den Profilen
+entfernt (Vor-, Nach- und Anzeigename bleiben stehen); wird ein Benutzer in WordPress gelöscht, wird auch sein Bild gelöscht.
**Deaktivierte Konten** können sich überhaupt nicht mehr anmelden – weder per Microsoft noch per Passwort,
Anwendungspasswort oder bestehender Session (alle Sessions werden beendet). In der Benutzerliste zeigt die Spalte
diff --git a/includes/class-m365-login-admin.php b/includes/class-m365-login-admin.php
index 6228ea4..6c6ef67 100644
--- a/includes/class-m365-login-admin.php
+++ b/includes/class-m365-login-admin.php
@@ -640,7 +640,7 @@ class M365_Login_Admin {
-
+
diff --git a/includes/class-m365-login-graph.php b/includes/class-m365-login-graph.php
index 027c485..f4f8bfb 100644
--- a/includes/class-m365-login-graph.php
+++ b/includes/class-m365-login-graph.php
@@ -277,52 +277,111 @@ class M365_Login_Graph {
}
/**
- * Metadata of a user's profile photo (prefers the 240×240 rendition).
+ * Runs up to 20 GET requests in one Graph JSON batch.
+ *
+ * @param string[] $paths Request key => path relative to the v1.0 base.
+ * @return array|WP_Error Request key => array( 'status' => int, 'body' => mixed ).
+ */
+ public function batch_get( $paths ) {
+ $requests = array();
+ foreach ( array_values( $paths ) as $i => $path ) {
+ $requests[] = array(
+ 'id' => (string) $i,
+ 'method' => 'GET',
+ 'url' => $path,
+ );
+ }
+ $keys = array_keys( $paths );
+ if ( empty( $requests ) ) {
+ return array();
+ }
+ if ( count( $requests ) > 20 ) {
+ return new WP_Error( 'graph_batch_size', 'A Graph batch holds at most 20 requests.' );
+ }
+
+ $result = $this->request( 'POST', '/$batch', array( 'requests' => $requests ) );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ $out = array();
+ foreach ( isset( $result['responses'] ) && is_array( $result['responses'] ) ? $result['responses'] : array() as $response ) {
+ $i = isset( $response['id'] ) ? (int) $response['id'] : -1;
+ if ( isset( $keys[ $i ] ) ) {
+ $out[ $keys[ $i ] ] = array(
+ 'status' => isset( $response['status'] ) ? (int) $response['status'] : 0,
+ 'body' => isset( $response['body'] ) ? $response['body'] : null,
+ );
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Profile photo versions of several users (one batch request per 20 users).
+ *
+ * @param string[] $oids User object IDs.
+ * @return array oid => etag string, null (user has no photo) or WP_Error (could not be checked).
+ */
+ public function photo_versions( $oids ) {
+ $out = array();
+ foreach ( array_chunk( array_values( array_filter( $oids, array( 'M365_Login_Settings', 'is_guid' ) ) ), 20 ) as $chunk ) {
+ $paths = array();
+ foreach ( $chunk as $oid ) {
+ $paths[ $oid ] = '/users/' . rawurlencode( strtolower( $oid ) ) . '/photo';
+ }
+ $responses = $this->batch_get( $paths );
+ foreach ( $chunk as $oid ) {
+ if ( is_wp_error( $responses ) ) {
+ $out[ $oid ] = $responses;
+ continue;
+ }
+ $response = isset( $responses[ $oid ] ) ? $responses[ $oid ] : array(
+ 'status' => 0,
+ 'body' => null,
+ );
+ if ( 404 === $response['status'] ) {
+ $out[ $oid ] = null;
+ } elseif ( 200 === $response['status'] && is_array( $response['body'] ) ) {
+ $etag = isset( $response['body']['@odata.mediaEtag'] ) ? (string) $response['body']['@odata.mediaEtag'] : '';
+ $out[ $oid ] = '' !== $etag ? $etag : md5( (string) wp_json_encode( $response['body'] ) );
+ } else {
+ $code = isset( $response['body']['error']['code'] ) ? (string) $response['body']['error']['code'] : 'HTTP ' . $response['status'];
+ $out[ $oid ] = new WP_Error( 'graph_photo', $code, array( 'status' => $response['status'] ) );
+ }
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Downloads a user's photo (240×240 rendition, else the original).
*
* @param string $oid User object ID.
- * @return array|null|WP_Error array( 'path' => photo path, 'etag' => string ), null when the user has no photo.
+ * @return string|null|WP_Error Binary image data, null when the user has no photo.
*/
- public function photo_info( $oid ) {
+ public function photo_bytes( $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;
+ foreach ( array( $base . '/photos/240x240/$value', $base . '/photo/$value' ) as $path ) {
+ $response = $this->raw_request( 'GET', $path, 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 $body;
+ }
+ if ( 404 !== $code ) {
+ return $this->error_from( $code, json_decode( $body, true ) );
}
- $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-sync.php b/includes/class-m365-login-sync.php
index 5eb1022..347b23a 100644
--- a/includes/class-m365-login-sync.php
+++ b/includes/class-m365-login-sync.php
@@ -75,6 +75,7 @@ class M365_Login_Sync {
add_filter( 'determine_current_user', array( $this, 'drop_disabled_session' ), 100 );
add_filter( 'pre_get_avatar_data', array( $this, 'avatar_data' ), 10, 2 );
+ add_action( 'delete_user', array( $this, 'delete_photo' ) );
if ( is_admin() ) {
add_filter( 'manage_users_columns', array( $this, 'users_column' ) );
@@ -394,7 +395,7 @@ class M365_Login_Sync {
$linked = $this->linked_users();
$seen = array();
$pending = array(); // Deactivations/deletions, applied after the safety check.
- $photos = 0;
+ $photo_of = array(); // oid => WP_User whose photo is kept in sync.
$photo_on = array_key_exists( 'photo', $this->selected_attributes() );
foreach ( $people as $person ) {
@@ -404,11 +405,18 @@ class M365_Login_Sync {
$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 );
+ } elseif ( $result instanceof WP_User ) {
+ $photo_of[ $oid ] = $result;
}
}
+ // Profile photos: new, changed and removed photos, or cleanup when the photo sync was switched off.
+ if ( $photo_on ) {
+ $this->sync_photos( $photo_of );
+ } else {
+ $this->remove_all_photos();
+ }
+
// 3. Linked accounts that were not part of the directory listing.
foreach ( $linked as $oid => $user_id ) {
if ( isset( $seen[ $oid ] ) ) {
@@ -776,6 +784,22 @@ class M365_Login_Sync {
}
}
+ // Fields that are no longer selected are removed from the profile (only the plugin's own m365_* keys).
+ $selected = $this->selected_attributes();
+ foreach ( $labels as $key => $attribute ) {
+ $target = isset( $attribute['target'] ) ? (string) $attribute['target'] : '';
+ if ( isset( $selected[ $key ] ) || 0 !== strpos( $target, 'm365_' ) ) {
+ continue;
+ }
+ if ( '' !== (string) get_user_meta( $user->ID, $target, true ) ) {
+ if ( ! $this->dry ) {
+ delete_user_meta( $user->ID, $target );
+ }
+ /* translators: %s: profile field */
+ $changes[] = sprintf( __( '%s removed', 'm365-login' ), $attribute['label'] );
+ }
+ }
+
if ( $fields && ! $this->dry ) {
$fields['ID'] = $user->ID;
$result = wp_update_user( $fields );
@@ -996,8 +1020,7 @@ class M365_Login_Sync {
$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 );
+ wp_delete_user( $user->ID, $reassign ); // The delete_user hook removes the photo.
}
return;
}
@@ -1133,89 +1156,182 @@ class M365_Login_Sync {
/* ------------------------------------------------------------------ */
/**
- * Refreshes the profile photo when it was not checked recently.
+ * Brings the stored photos in line with Microsoft 365: downloads new and changed
+ * photos and deletes photos that were removed in Microsoft 365.
*
- * @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.
+ * Photo versions are compared on every run (20 users per Graph batch request);
+ * only changed photos are downloaded.
+ *
+ * @param WP_User[] $users oid => user.
*/
- private function maybe_sync_photo( $user, $oid, $done_so_far ) {
+ private function sync_photos( $users ) {
/**
- * 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.
+ * Minimum number of seconds between two photo checks of the same user (0 = every run).
*
* @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;
- }
+ $interval = (int) apply_filters( 'm365_login_sync_photo_interval', 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;
- }
+ /**
+ * Maximum number of photo downloads per sync run (the rest follows in later runs).
+ *
+ * @param int $limit Limit.
+ */
+ $limit = (int) apply_filters( 'm365_login_sync_photo_limit', 500 );
- 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' );
+ $check = array();
+ foreach ( $users as $oid => $user ) {
+ $stored = $this->stored_photo( $user->ID );
+ if ( $interval > 0 && ! empty( $stored['checked'] ) && time() - (int) $stored['checked'] < $interval ) {
+ continue;
}
+ $check[ $oid ] = $user;
+ }
+ if ( empty( $check ) ) {
+ return;
+ }
+
+ $versions = $this->graph->photo_versions( array_keys( $check ) );
+ $downloads = 0;
+ $deferred = 0;
+
+ foreach ( $check as $oid => $user ) {
+ $version = array_key_exists( $oid, $versions ) ? $versions[ $oid ] : new WP_Error( 'graph_photo', 'No answer.' );
+ $stored = $this->stored_photo( $user->ID );
+
+ // Never delete anything because of an error – only a clear "no photo" removes it.
+ if ( is_wp_error( $version ) ) {
+ /* 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, $version->get_error_message() ) );
+ continue;
+ }
+
+ if ( null === $version ) {
+ if ( ! empty( $stored['file'] ) ) {
+ $this->remove_photo( $user );
+ } elseif ( ! $this->dry ) {
+ update_user_meta( $user->ID, self::META_PHOTO, array( 'checked' => time() ) );
+ }
+ continue;
+ }
+
+ if ( ! empty( $stored['file'] ) && isset( $stored['etag'] ) && $stored['etag'] === $version && file_exists( self::photo_path( $stored['file'] ) ) ) {
+ if ( ! $this->dry ) {
+ $stored['checked'] = time();
+ update_user_meta( $user->ID, self::META_PHOTO, $stored );
+ }
+ continue;
+ }
+
+ // New or changed photo.
+ if ( $this->dry ) {
+ /* translators: %s: e-mail address */
+ $this->log( 'info', sprintf( __( '%s: profile photo updated.', 'm365-login' ), $user->user_email ) );
+ $this->count( 'photos' );
+ continue;
+ }
+ if ( $downloads >= $limit ) {
+ ++$deferred;
+ continue;
+ }
+ ++$downloads;
+
+ $bytes = $this->graph->photo_bytes( $oid );
+ if ( null === $bytes ) {
+ if ( ! empty( $stored['file'] ) ) {
+ $this->remove_photo( $user );
+ }
+ continue;
+ }
+ 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 ) );
+ continue;
+ }
+
+ $file = $this->store_photo( $user->ID, $oid, $version, $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 ) );
+ continue;
+ }
+
+ 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' => $version,
+ 'checked' => time(),
+ )
+ );
+ /* translators: %s: e-mail address */
+ $this->log( 'info', sprintf( __( '%s: profile photo updated.', 'm365-login' ), $user->user_email ) );
+ $this->count( 'photos' );
+ }
+
+ if ( $deferred ) {
+ /* translators: %d: number of photos */
+ $this->log( 'info', sprintf( _n( '%d changed profile photo will be downloaded in the next run (download limit per run reached).', '%d changed profile photos will be downloaded in the next run (download limit per run reached).', $deferred, 'm365-login' ), $deferred ) );
+ }
+ }
+
+ /**
+ * Removes the stored photo of a user whose photo was deleted in Microsoft 365.
+ *
+ * @param WP_User $user User.
+ */
+ private function remove_photo( $user ) {
+ if ( ! $this->dry ) {
+ $this->delete_photo( $user->ID );
update_user_meta( $user->ID, self::META_PHOTO, array( 'checked' => time() ) );
- return 1;
}
+ /* translators: %s: e-mail address */
+ $this->log( 'info', sprintf( __( '%s: profile photo removed.', 'm365-login' ), $user->user_email ) );
+ $this->count( 'photos' );
+ }
- 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,
+ /**
+ * Deletes every stored photo (the photo sync was switched off).
+ */
+ private function remove_all_photos() {
+ $users = get_users(
array(
- 'file' => $file,
- 'etag' => $info['etag'],
- 'checked' => time(),
+ 'meta_key' => self::META_PHOTO, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_compare' => 'EXISTS',
+ 'fields' => array( 'ID' ),
+ 'number' => -1,
)
);
- /* translators: %s: e-mail address */
- $this->log( 'info', sprintf( __( '%s: profile photo updated.', 'm365-login' ), $user->user_email ) );
- $this->count( 'photos' );
- return 1;
+ $removed = 0;
+ foreach ( $users as $row ) {
+ $stored = $this->stored_photo( (int) $row->ID );
+ if ( ! empty( $stored['file'] ) ) {
+ ++$removed;
+ }
+ if ( ! $this->dry ) {
+ $this->delete_photo( (int) $row->ID );
+ }
+ }
+ if ( $removed ) {
+ /* translators: %d: number of photos */
+ $this->log( 'info', sprintf( _n( 'Profile photo sync is off: %d stored photo removed.', 'Profile photo sync is off: %d stored photos removed.', $removed, 'm365-login' ), $removed ) );
+ $this->count( 'photos', $removed );
+ }
+ }
+
+ /**
+ * Stored photo record of a user.
+ *
+ * @param int $user_id User ID.
+ * @return array
+ */
+ private function stored_photo( $user_id ) {
+ $stored = get_user_meta( $user_id, self::META_PHOTO, true );
+ return is_array( $stored ) ? $stored : array();
}
/**
@@ -1273,11 +1389,11 @@ class M365_Login_Sync {
}
/**
- * Deletes a user's stored photo.
+ * Deletes a user's stored photo (also hooked to user deletion).
*
* @param int $user_id User ID.
*/
- private function delete_photo( $user_id ) {
+ public 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'] ) );
diff --git a/languages/m365-login-de_DE.mo b/languages/m365-login-de_DE.mo
index dab20df..982d310 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 0386eb1..4efe142 100644
--- a/languages/m365-login-de_DE.po
+++ b/languages/m365-login-de_DE.po
@@ -124,7 +124,7 @@ msgstr "Du hast ungespeicherte Änderungen. Der Sync verwendet die gespeicherten
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
+#: 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:1611
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
@@ -428,8 +428,8 @@ msgid "Selected Microsoft 365 attributes are copied into the WordPress profile o
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."
+msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are compared on every run: changed photos are downloaded again, photos deleted in Microsoft 365 are deleted in WordPress too. Fields and photos you deselect here are removed from the profiles on the next run (first and last name and display name stay)."
+msgstr "Profilbilder werden in wp-content/uploads/m365-login-avatars/ gespeichert und ersetzen den Gravatar. Sie werden bei jedem Lauf abgeglichen: Geänderte Bilder werden neu geladen, in Microsoft 365 gelöschte Bilder auch in WordPress gelöscht. Felder und Bilder, die du hier abwählst, werden beim nächsten Lauf aus den Profilen entfernt (Vor-, Nach- und Anzeigename bleiben)."
#: includes/class-m365-login-admin.php:647
msgid "Disabled and deleted Microsoft 365 accounts"
@@ -1119,7 +1119,7 @@ msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
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
+#: includes/class-m365-login-auth.php:864 includes/class-m365-login-sync.php:1520
msgid "This account has been deactivated."
msgstr "Dieses Konto wurde deaktiviert."
@@ -1175,15 +1175,15 @@ 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:375
+#: includes/class-m365-login-graph.php:434
msgid "Group"
msgstr "Gruppe"
-#: includes/class-m365-login-graph.php:377
+#: includes/class-m365-login-graph.php:436
msgid "Security group"
msgstr "Sicherheitsgruppe"
-#: includes/class-m365-login-graph.php:379
+#: includes/class-m365-login-graph.php:438
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
@@ -1235,293 +1235,312 @@ msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
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
+#: includes/class-m365-login-sync.php:110
msgid "Display name"
msgstr "Anzeigename"
-#: includes/class-m365-login-sync.php:113
+#: includes/class-m365-login-sync.php:114
msgid "First name"
msgstr "Vorname"
-#: includes/class-m365-login-sync.php:117
+#: includes/class-m365-login-sync.php:118
msgid "Last name"
msgstr "Nachname"
-#: includes/class-m365-login-sync.php:121
+#: includes/class-m365-login-sync.php:122
msgid "Profile photo (used as avatar)"
msgstr "Profilbild (als Avatar)"
-#: includes/class-m365-login-sync.php:125
+#: includes/class-m365-login-sync.php:126
msgid "Job title"
msgstr "Position"
-#: includes/class-m365-login-sync.php:129
+#: includes/class-m365-login-sync.php:130
msgid "Department"
msgstr "Abteilung"
-#: includes/class-m365-login-sync.php:133
+#: includes/class-m365-login-sync.php:134
msgid "Company"
msgstr "Firma"
-#: includes/class-m365-login-sync.php:137
+#: includes/class-m365-login-sync.php:138
msgid "Office"
msgstr "Büro"
-#: includes/class-m365-login-sync.php:141
+#: includes/class-m365-login-sync.php:142
msgid "Employee ID"
msgstr "Personalnummer"
-#: includes/class-m365-login-sync.php:145
+#: includes/class-m365-login-sync.php:146
msgid "Business phone"
msgstr "Telefon (geschäftlich)"
-#: includes/class-m365-login-sync.php:149
+#: includes/class-m365-login-sync.php:150
msgid "Mobile phone"
msgstr "Mobiltelefon"
-#: includes/class-m365-login-sync.php:153
+#: includes/class-m365-login-sync.php:154
msgid "Street address"
msgstr "Straße"
-#: includes/class-m365-login-sync.php:157
+#: includes/class-m365-login-sync.php:158
msgid "Postal code"
msgstr "Postleitzahl"
-#: includes/class-m365-login-sync.php:161
+#: includes/class-m365-login-sync.php:162
msgid "City"
msgstr "Ort"
-#: includes/class-m365-login-sync.php:165
+#: includes/class-m365-login-sync.php:166
msgid "State / province"
msgstr "Bundesland / Region"
-#: includes/class-m365-login-sync.php:169
+#: includes/class-m365-login-sync.php:170
msgid "Country"
msgstr "Land"
-#: includes/class-m365-login-sync.php:173
+#: includes/class-m365-login-sync.php:174
msgid "Language (sets the admin language if installed)"
msgstr "Sprache (setzt die Backend-Sprache, falls installiert)"
-#: includes/class-m365-login-sync.php:308
+#: includes/class-m365-login-sync.php:309
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
+#: includes/class-m365-login-sync.php:366
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
+#: includes/class-m365-login-sync.php:370
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
+#: includes/class-m365-login-sync.php:374
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
+#: includes/class-m365-login-sync.php:386
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
+#: includes/class-m365-login-sync.php:443
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
+#: includes/class-m365-login-sync.php:567
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
+#: includes/class-m365-login-sync.php:574
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
+#: includes/class-m365-login-sync.php:586
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
+#: includes/class-m365-login-sync.php:590
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
+#: includes/class-m365-login-sync.php:604 includes/class-m365-login-sync.php:955 includes/class-m365-login-sync.php:1652
msgid "disabled in Microsoft 365"
msgstr "in Microsoft 365 deaktiviert"
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:607
+#: includes/class-m365-login-sync.php:615
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
+#: includes/class-m365-login-sync.php:624
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
+#: includes/class-m365-login-sync.php:635
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
+#: includes/class-m365-login-sync.php:661
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
+#: includes/class-m365-login-sync.php:688
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
+#: includes/class-m365-login-sync.php:746
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
+#: includes/class-m365-login-sync.php:749
msgid "e-mail"
msgstr "E-Mail"
+#. translators: %s: profile field
+#: includes/class-m365-login-sync.php:799
+msgid "%s removed"
+msgstr "%s entfernt"
+
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:785
+#: includes/class-m365-login-sync.php:809
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
+#: includes/class-m365-login-sync.php:915
msgid "roles: %s"
msgstr "Rollen: %s"
-#: includes/class-m365-login-sync.php:926 includes/class-m365-login-sync.php:1537
+#: includes/class-m365-login-sync.php:950 includes/class-m365-login-sync.php:1653
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
+#: includes/class-m365-login-sync.php:958 includes/class-m365-login-sync.php:1654
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
+#: includes/class-m365-login-sync.php:978
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
+#: includes/class-m365-login-sync.php:1011
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
+#: includes/class-m365-login-sync.php:1020
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
+#: includes/class-m365-login-sync.php:1029
msgid "%1$s: account deactivated (%2$s)."
msgstr "%1$s: Konto deaktiviert (%2$s)."
-#: includes/class-m365-login-sync.php:1085
+#: includes/class-m365-login-sync.php:1108
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
+#: includes/class-m365-login-sync.php:1111
msgid "Microsoft Graph error: %s"
msgstr "Microsoft-Graph-Fehler: %s"
-#: includes/class-m365-login-sync.php:1106
+#: includes/class-m365-login-sync.php:1129
msgid "Log truncated."
msgstr "Protokoll gekürzt."
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:1168
+#: includes/class-m365-login-sync.php:1205
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."
+#: includes/class-m365-login-sync.php:1229 includes/class-m365-login-sync.php:1272
+msgid "%s: profile photo updated."
+msgstr "%s: Profilbild aktualisiert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1192
+#: includes/class-m365-login-sync.php:1248
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
+#: includes/class-m365-login-sync.php:1255
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."
+#. translators: %d: number of photos
+#: includes/class-m365-login-sync.php:1278
+msgid "%d changed profile photo will be downloaded in the next run (download limit per run reached)."
+msgid_plural "%d changed profile photos will be downloaded in the next run (download limit per run reached)."
+msgstr[0] "%d geändertes Profilbild wird im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
+msgstr[1] "%d geänderte Profilbilder werden im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
-#: includes/class-m365-login-sync.php:1433 includes/class-m365-login-sync.php:1566
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1293
+msgid "%s: profile photo removed."
+msgstr "%s: Profilbild entfernt."
+
+#. translators: %d: number of photos
+#: includes/class-m365-login-sync.php:1321
+msgid "Profile photo sync is off: %d stored photo removed."
+msgid_plural "Profile photo sync is off: %d stored photos removed."
+msgstr[0] "Profilbild-Sync ist aus: %d gespeichertes Profilbild entfernt."
+msgstr[1] "Profilbild-Sync ist aus: %d gespeicherte Profilbilder entfernt."
+
+#: includes/class-m365-login-sync.php:1549 includes/class-m365-login-sync.php:1682
msgid "Microsoft 365"
msgstr "Microsoft 365"
-#: includes/class-m365-login-sync.php:1451
+#: includes/class-m365-login-sync.php:1567
msgid "Deactivated"
msgstr "Deaktiviert"
-#: includes/class-m365-login-sync.php:1454
+#: includes/class-m365-login-sync.php:1570
msgid "Imported"
msgstr "Importiert"
-#: includes/class-m365-login-sync.php:1456
+#: includes/class-m365-login-sync.php:1572
msgid "Linked"
msgstr "Verknüpft"
-#: includes/class-m365-login-sync.php:1484
+#: includes/class-m365-login-sync.php:1600
msgid "Reactivate"
msgstr "Reaktivieren"
-#: includes/class-m365-login-sync.php:1484
+#: includes/class-m365-login-sync.php:1600
msgid "Deactivate"
msgstr "Deaktivieren"
-#: includes/class-m365-login-sync.php:1517
+#: includes/class-m365-login-sync.php:1633
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
+#: includes/class-m365-login-sync.php:1634
msgid "The account has been reactivated."
msgstr "Das Konto wurde reaktiviert."
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1542
+#: includes/class-m365-login-sync.php:1658
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
+#: includes/class-m365-login-sync.php:1660
msgid "manually"
msgstr "manuell"
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1546
+#: includes/class-m365-login-sync.php:1662
msgid "Status"
msgstr "Status"
-#: includes/class-m365-login-sync.php:1549
+#: includes/class-m365-login-sync.php:1665
msgid "Object ID"
msgstr "Objekt-ID"
-#: includes/class-m365-login-sync.php:1553
+#: includes/class-m365-login-sync.php:1669
msgid "Last sync"
msgstr "Letzter Sync"
-#: includes/class-m365-login-sync.php:1575
+#: includes/class-m365-login-sync.php:1691
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."
diff --git a/languages/m365-login-de_DE_formal.mo b/languages/m365-login-de_DE_formal.mo
index f404ef3..d7269bd 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 eee0dbc..2a828fa 100644
--- a/languages/m365-login-de_DE_formal.po
+++ b/languages/m365-login-de_DE_formal.po
@@ -124,7 +124,7 @@ msgstr "Sie haben ungespeicherte Änderungen. Der Sync verwendet die gespeichert
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
+#: 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:1611
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
@@ -428,8 +428,8 @@ msgid "Selected Microsoft 365 attributes are copied into the WordPress profile o
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."
+msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are compared on every run: changed photos are downloaded again, photos deleted in Microsoft 365 are deleted in WordPress too. Fields and photos you deselect here are removed from the profiles on the next run (first and last name and display name stay)."
+msgstr "Profilbilder werden in wp-content/uploads/m365-login-avatars/ gespeichert und ersetzen den Gravatar. Sie werden bei jedem Lauf abgeglichen: Geänderte Bilder werden neu geladen, in Microsoft 365 gelöschte Bilder auch in WordPress gelöscht. Felder und Bilder, die Sie hier abwählen, werden beim nächsten Lauf aus den Profilen entfernt (Vor-, Nach- und Anzeigename bleiben)."
#: includes/class-m365-login-admin.php:647
msgid "Disabled and deleted Microsoft 365 accounts"
@@ -1119,7 +1119,7 @@ msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
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
+#: includes/class-m365-login-auth.php:864 includes/class-m365-login-sync.php:1520
msgid "This account has been deactivated."
msgstr "Dieses Konto wurde deaktiviert."
@@ -1175,15 +1175,15 @@ 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:375
+#: includes/class-m365-login-graph.php:434
msgid "Group"
msgstr "Gruppe"
-#: includes/class-m365-login-graph.php:377
+#: includes/class-m365-login-graph.php:436
msgid "Security group"
msgstr "Sicherheitsgruppe"
-#: includes/class-m365-login-graph.php:379
+#: includes/class-m365-login-graph.php:438
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
@@ -1235,293 +1235,312 @@ msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
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
+#: includes/class-m365-login-sync.php:110
msgid "Display name"
msgstr "Anzeigename"
-#: includes/class-m365-login-sync.php:113
+#: includes/class-m365-login-sync.php:114
msgid "First name"
msgstr "Vorname"
-#: includes/class-m365-login-sync.php:117
+#: includes/class-m365-login-sync.php:118
msgid "Last name"
msgstr "Nachname"
-#: includes/class-m365-login-sync.php:121
+#: includes/class-m365-login-sync.php:122
msgid "Profile photo (used as avatar)"
msgstr "Profilbild (als Avatar)"
-#: includes/class-m365-login-sync.php:125
+#: includes/class-m365-login-sync.php:126
msgid "Job title"
msgstr "Position"
-#: includes/class-m365-login-sync.php:129
+#: includes/class-m365-login-sync.php:130
msgid "Department"
msgstr "Abteilung"
-#: includes/class-m365-login-sync.php:133
+#: includes/class-m365-login-sync.php:134
msgid "Company"
msgstr "Firma"
-#: includes/class-m365-login-sync.php:137
+#: includes/class-m365-login-sync.php:138
msgid "Office"
msgstr "Büro"
-#: includes/class-m365-login-sync.php:141
+#: includes/class-m365-login-sync.php:142
msgid "Employee ID"
msgstr "Personalnummer"
-#: includes/class-m365-login-sync.php:145
+#: includes/class-m365-login-sync.php:146
msgid "Business phone"
msgstr "Telefon (geschäftlich)"
-#: includes/class-m365-login-sync.php:149
+#: includes/class-m365-login-sync.php:150
msgid "Mobile phone"
msgstr "Mobiltelefon"
-#: includes/class-m365-login-sync.php:153
+#: includes/class-m365-login-sync.php:154
msgid "Street address"
msgstr "Straße"
-#: includes/class-m365-login-sync.php:157
+#: includes/class-m365-login-sync.php:158
msgid "Postal code"
msgstr "Postleitzahl"
-#: includes/class-m365-login-sync.php:161
+#: includes/class-m365-login-sync.php:162
msgid "City"
msgstr "Ort"
-#: includes/class-m365-login-sync.php:165
+#: includes/class-m365-login-sync.php:166
msgid "State / province"
msgstr "Bundesland / Region"
-#: includes/class-m365-login-sync.php:169
+#: includes/class-m365-login-sync.php:170
msgid "Country"
msgstr "Land"
-#: includes/class-m365-login-sync.php:173
+#: includes/class-m365-login-sync.php:174
msgid "Language (sets the admin language if installed)"
msgstr "Sprache (setzt die Backend-Sprache, falls installiert)"
-#: includes/class-m365-login-sync.php:308
+#: includes/class-m365-login-sync.php:309
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
+#: includes/class-m365-login-sync.php:366
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
+#: includes/class-m365-login-sync.php:370
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
+#: includes/class-m365-login-sync.php:374
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
+#: includes/class-m365-login-sync.php:386
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
+#: includes/class-m365-login-sync.php:443
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
+#: includes/class-m365-login-sync.php:567
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
+#: includes/class-m365-login-sync.php:574
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
+#: includes/class-m365-login-sync.php:586
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
+#: includes/class-m365-login-sync.php:590
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
+#: includes/class-m365-login-sync.php:604 includes/class-m365-login-sync.php:955 includes/class-m365-login-sync.php:1652
msgid "disabled in Microsoft 365"
msgstr "in Microsoft 365 deaktiviert"
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:607
+#: includes/class-m365-login-sync.php:615
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
+#: includes/class-m365-login-sync.php:624
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
+#: includes/class-m365-login-sync.php:635
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
+#: includes/class-m365-login-sync.php:661
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
+#: includes/class-m365-login-sync.php:688
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
+#: includes/class-m365-login-sync.php:746
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
+#: includes/class-m365-login-sync.php:749
msgid "e-mail"
msgstr "E-Mail"
+#. translators: %s: profile field
+#: includes/class-m365-login-sync.php:799
+msgid "%s removed"
+msgstr "%s entfernt"
+
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:785
+#: includes/class-m365-login-sync.php:809
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
+#: includes/class-m365-login-sync.php:915
msgid "roles: %s"
msgstr "Rollen: %s"
-#: includes/class-m365-login-sync.php:926 includes/class-m365-login-sync.php:1537
+#: includes/class-m365-login-sync.php:950 includes/class-m365-login-sync.php:1653
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
+#: includes/class-m365-login-sync.php:958 includes/class-m365-login-sync.php:1654
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
+#: includes/class-m365-login-sync.php:978
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
+#: includes/class-m365-login-sync.php:1011
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
+#: includes/class-m365-login-sync.php:1020
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
+#: includes/class-m365-login-sync.php:1029
msgid "%1$s: account deactivated (%2$s)."
msgstr "%1$s: Konto deaktiviert (%2$s)."
-#: includes/class-m365-login-sync.php:1085
+#: includes/class-m365-login-sync.php:1108
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
+#: includes/class-m365-login-sync.php:1111
msgid "Microsoft Graph error: %s"
msgstr "Microsoft-Graph-Fehler: %s"
-#: includes/class-m365-login-sync.php:1106
+#: includes/class-m365-login-sync.php:1129
msgid "Log truncated."
msgstr "Protokoll gekürzt."
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:1168
+#: includes/class-m365-login-sync.php:1205
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."
+#: includes/class-m365-login-sync.php:1229 includes/class-m365-login-sync.php:1272
+msgid "%s: profile photo updated."
+msgstr "%s: Profilbild aktualisiert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1192
+#: includes/class-m365-login-sync.php:1248
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
+#: includes/class-m365-login-sync.php:1255
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."
+#. translators: %d: number of photos
+#: includes/class-m365-login-sync.php:1278
+msgid "%d changed profile photo will be downloaded in the next run (download limit per run reached)."
+msgid_plural "%d changed profile photos will be downloaded in the next run (download limit per run reached)."
+msgstr[0] "%d geändertes Profilbild wird im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
+msgstr[1] "%d geänderte Profilbilder werden im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
-#: includes/class-m365-login-sync.php:1433 includes/class-m365-login-sync.php:1566
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:1293
+msgid "%s: profile photo removed."
+msgstr "%s: Profilbild entfernt."
+
+#. translators: %d: number of photos
+#: includes/class-m365-login-sync.php:1321
+msgid "Profile photo sync is off: %d stored photo removed."
+msgid_plural "Profile photo sync is off: %d stored photos removed."
+msgstr[0] "Profilbild-Sync ist aus: %d gespeichertes Profilbild entfernt."
+msgstr[1] "Profilbild-Sync ist aus: %d gespeicherte Profilbilder entfernt."
+
+#: includes/class-m365-login-sync.php:1549 includes/class-m365-login-sync.php:1682
msgid "Microsoft 365"
msgstr "Microsoft 365"
-#: includes/class-m365-login-sync.php:1451
+#: includes/class-m365-login-sync.php:1567
msgid "Deactivated"
msgstr "Deaktiviert"
-#: includes/class-m365-login-sync.php:1454
+#: includes/class-m365-login-sync.php:1570
msgid "Imported"
msgstr "Importiert"
-#: includes/class-m365-login-sync.php:1456
+#: includes/class-m365-login-sync.php:1572
msgid "Linked"
msgstr "Verknüpft"
-#: includes/class-m365-login-sync.php:1484
+#: includes/class-m365-login-sync.php:1600
msgid "Reactivate"
msgstr "Reaktivieren"
-#: includes/class-m365-login-sync.php:1484
+#: includes/class-m365-login-sync.php:1600
msgid "Deactivate"
msgstr "Deaktivieren"
-#: includes/class-m365-login-sync.php:1517
+#: includes/class-m365-login-sync.php:1633
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
+#: includes/class-m365-login-sync.php:1634
msgid "The account has been reactivated."
msgstr "Das Konto wurde reaktiviert."
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1542
+#: includes/class-m365-login-sync.php:1658
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
+#: includes/class-m365-login-sync.php:1660
msgid "manually"
msgstr "manuell"
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1546
+#: includes/class-m365-login-sync.php:1662
msgid "Status"
msgstr "Status"
-#: includes/class-m365-login-sync.php:1549
+#: includes/class-m365-login-sync.php:1665
msgid "Object ID"
msgstr "Objekt-ID"
-#: includes/class-m365-login-sync.php:1553
+#: includes/class-m365-login-sync.php:1669
msgid "Last sync"
msgstr "Letzter Sync"
-#: includes/class-m365-login-sync.php:1575
+#: includes/class-m365-login-sync.php:1691
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."
diff --git a/languages/m365-login.pot b/languages/m365-login.pot
index e861616..299d0c8 100644
--- a/languages/m365-login.pot
+++ b/languages/m365-login.pot
@@ -122,7 +122,7 @@ msgstr ""
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
+#: 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:1611
msgid "You are not allowed to do this."
msgstr ""
@@ -426,7 +426,7 @@ msgid "Selected Microsoft 365 attributes are copied into the WordPress profile o
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."
+msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are compared on every run: changed photos are downloaded again, photos deleted in Microsoft 365 are deleted in WordPress too. Fields and photos you deselect here are removed from the profiles on the next run (first and last name and display name stay)."
msgstr ""
#: includes/class-m365-login-admin.php:647
@@ -1117,7 +1117,7 @@ msgstr ""
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
+#: includes/class-m365-login-auth.php:864 includes/class-m365-login-sync.php:1520
msgid "This account has been deactivated."
msgstr ""
@@ -1173,15 +1173,15 @@ msgstr ""
msgid "The certificate has already expired."
msgstr ""
-#: includes/class-m365-login-graph.php:375
+#: includes/class-m365-login-graph.php:434
msgid "Group"
msgstr ""
-#: includes/class-m365-login-graph.php:377
+#: includes/class-m365-login-graph.php:436
msgid "Security group"
msgstr ""
-#: includes/class-m365-login-graph.php:379
+#: includes/class-m365-login-graph.php:438
msgid "Microsoft 365 group"
msgstr ""
@@ -1233,293 +1233,312 @@ msgstr ""
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
+#: includes/class-m365-login-sync.php:110
msgid "Display name"
msgstr ""
-#: includes/class-m365-login-sync.php:113
+#: includes/class-m365-login-sync.php:114
msgid "First name"
msgstr ""
-#: includes/class-m365-login-sync.php:117
+#: includes/class-m365-login-sync.php:118
msgid "Last name"
msgstr ""
-#: includes/class-m365-login-sync.php:121
+#: includes/class-m365-login-sync.php:122
msgid "Profile photo (used as avatar)"
msgstr ""
-#: includes/class-m365-login-sync.php:125
+#: includes/class-m365-login-sync.php:126
msgid "Job title"
msgstr ""
-#: includes/class-m365-login-sync.php:129
+#: includes/class-m365-login-sync.php:130
msgid "Department"
msgstr ""
-#: includes/class-m365-login-sync.php:133
+#: includes/class-m365-login-sync.php:134
msgid "Company"
msgstr ""
-#: includes/class-m365-login-sync.php:137
+#: includes/class-m365-login-sync.php:138
msgid "Office"
msgstr ""
-#: includes/class-m365-login-sync.php:141
+#: includes/class-m365-login-sync.php:142
msgid "Employee ID"
msgstr ""
-#: includes/class-m365-login-sync.php:145
+#: includes/class-m365-login-sync.php:146
msgid "Business phone"
msgstr ""
-#: includes/class-m365-login-sync.php:149
+#: includes/class-m365-login-sync.php:150
msgid "Mobile phone"
msgstr ""
-#: includes/class-m365-login-sync.php:153
+#: includes/class-m365-login-sync.php:154
msgid "Street address"
msgstr ""
-#: includes/class-m365-login-sync.php:157
+#: includes/class-m365-login-sync.php:158
msgid "Postal code"
msgstr ""
-#: includes/class-m365-login-sync.php:161
+#: includes/class-m365-login-sync.php:162
msgid "City"
msgstr ""
-#: includes/class-m365-login-sync.php:165
+#: includes/class-m365-login-sync.php:166
msgid "State / province"
msgstr ""
-#: includes/class-m365-login-sync.php:169
+#: includes/class-m365-login-sync.php:170
msgid "Country"
msgstr ""
-#: includes/class-m365-login-sync.php:173
+#: includes/class-m365-login-sync.php:174
msgid "Language (sets the admin language if installed)"
msgstr ""
-#: includes/class-m365-login-sync.php:308
+#: includes/class-m365-login-sync.php:309
msgid "Another sync is still running. Please try again in a few minutes."
msgstr ""
-#: includes/class-m365-login-sync.php:365
+#: includes/class-m365-login-sync.php:366
msgid "The connection to Microsoft Entra ID is not configured yet."
msgstr ""
-#: includes/class-m365-login-sync.php:369
+#: includes/class-m365-login-sync.php:370
msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
msgstr ""
-#: includes/class-m365-login-sync.php:373
+#: includes/class-m365-login-sync.php:374
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
+#: includes/class-m365-login-sync.php:386
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
+#: includes/class-m365-login-sync.php:443
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
+#: includes/class-m365-login-sync.php:567
msgid "%s: no usable e-mail address, skipped."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:566
+#: includes/class-m365-login-sync.php:574
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
+#: includes/class-m365-login-sync.php:586
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
+#: includes/class-m365-login-sync.php:590
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
+#: includes/class-m365-login-sync.php:604 includes/class-m365-login-sync.php:955 includes/class-m365-login-sync.php:1652
msgid "disabled in Microsoft 365"
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:607
+#: includes/class-m365-login-sync.php:615
msgid "%s: added to this site."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:616
+#: includes/class-m365-login-sync.php:624
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
+#: includes/class-m365-login-sync.php:635
msgid "%1$s: updated (%2$s)."
msgstr ""
#. translators: 1: e-mail address, 2: role names
-#: includes/class-m365-login-sync.php:653
+#: includes/class-m365-login-sync.php:661
msgid "%1$s: account created (%2$s)."
msgstr ""
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:680
+#: includes/class-m365-login-sync.php:688
msgid "%1$s: account could not be created: %2$s"
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:738
+#: includes/class-m365-login-sync.php:746
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
+#: includes/class-m365-login-sync.php:749
msgid "e-mail"
msgstr ""
+#. translators: %s: profile field
+#: includes/class-m365-login-sync.php:799
+msgid "%s removed"
+msgstr ""
+
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:785
+#: includes/class-m365-login-sync.php:809
msgid "%1$s: profile could not be updated: %2$s"
msgstr ""
#. translators: %s: role names
-#: includes/class-m365-login-sync.php:891
+#: includes/class-m365-login-sync.php:915
msgid "roles: %s"
msgstr ""
-#: includes/class-m365-login-sync.php:926 includes/class-m365-login-sync.php:1537
+#: includes/class-m365-login-sync.php:950 includes/class-m365-login-sync.php:1653
msgid "deleted in Microsoft 365"
msgstr ""
-#: includes/class-m365-login-sync.php:934 includes/class-m365-login-sync.php:1538
+#: includes/class-m365-login-sync.php:958 includes/class-m365-login-sync.php:1654
msgid "no longer a member of the sync groups"
msgstr ""
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:954
+#: includes/class-m365-login-sync.php:978
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
+#: includes/class-m365-login-sync.php:1011
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
+#: includes/class-m365-login-sync.php:1020
msgid "%1$s: account deleted (%2$s)."
msgstr ""
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1006
+#: includes/class-m365-login-sync.php:1029
msgid "%1$s: account deactivated (%2$s)."
msgstr ""
-#: includes/class-m365-login-sync.php:1085
+#: includes/class-m365-login-sync.php:1108
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
+#: includes/class-m365-login-sync.php:1111
msgid "Microsoft Graph error: %s"
msgstr ""
-#: includes/class-m365-login-sync.php:1106
+#: includes/class-m365-login-sync.php:1129
msgid "Log truncated."
msgstr ""
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:1168
+#: includes/class-m365-login-sync.php:1205
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."
+#: includes/class-m365-login-sync.php:1229 includes/class-m365-login-sync.php:1272
+msgid "%s: profile photo updated."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1192
+#: includes/class-m365-login-sync.php:1248
msgid "%s: profile photo could not be downloaded."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1199
+#: includes/class-m365-login-sync.php:1255
msgid "%s: profile photo is not a valid image or could not be saved."
msgstr ""
+#. translators: %d: number of photos
+#: includes/class-m365-login-sync.php:1278
+msgid "%d changed profile photo will be downloaded in the next run (download limit per run reached)."
+msgid_plural "%d changed profile photos will be downloaded in the next run (download limit per run reached)."
+msgstr[0] ""
+msgstr[1] ""
+
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1216
-msgid "%s: profile photo updated."
+#: includes/class-m365-login-sync.php:1293
+msgid "%s: profile photo removed."
msgstr ""
-#: includes/class-m365-login-sync.php:1433 includes/class-m365-login-sync.php:1566
+#. translators: %d: number of photos
+#: includes/class-m365-login-sync.php:1321
+msgid "Profile photo sync is off: %d stored photo removed."
+msgid_plural "Profile photo sync is off: %d stored photos removed."
+msgstr[0] ""
+msgstr[1] ""
+
+#: includes/class-m365-login-sync.php:1549 includes/class-m365-login-sync.php:1682
msgid "Microsoft 365"
msgstr ""
-#: includes/class-m365-login-sync.php:1451
+#: includes/class-m365-login-sync.php:1567
msgid "Deactivated"
msgstr ""
-#: includes/class-m365-login-sync.php:1454
+#: includes/class-m365-login-sync.php:1570
msgid "Imported"
msgstr ""
-#: includes/class-m365-login-sync.php:1456
+#: includes/class-m365-login-sync.php:1572
msgid "Linked"
msgstr ""
-#: includes/class-m365-login-sync.php:1484
+#: includes/class-m365-login-sync.php:1600
msgid "Reactivate"
msgstr ""
-#: includes/class-m365-login-sync.php:1484
+#: includes/class-m365-login-sync.php:1600
msgid "Deactivate"
msgstr ""
-#: includes/class-m365-login-sync.php:1517
+#: includes/class-m365-login-sync.php:1633
msgid "The account has been deactivated and signed out everywhere."
msgstr ""
-#: includes/class-m365-login-sync.php:1518
+#: includes/class-m365-login-sync.php:1634
msgid "The account has been reactivated."
msgstr ""
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1542
+#: includes/class-m365-login-sync.php:1658
msgid "Deactivated since %1$s (%2$s)"
msgstr ""
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1544
+#: includes/class-m365-login-sync.php:1660
msgid "manually"
msgstr ""
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1546
+#: includes/class-m365-login-sync.php:1662
msgid "Status"
msgstr ""
-#: includes/class-m365-login-sync.php:1549
+#: includes/class-m365-login-sync.php:1665
msgid "Object ID"
msgstr ""
-#: includes/class-m365-login-sync.php:1553
+#: includes/class-m365-login-sync.php:1669
msgid "Last sync"
msgstr ""
-#: includes/class-m365-login-sync.php:1575
+#: includes/class-m365-login-sync.php:1691
msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
msgstr ""
diff --git a/readme.txt b/readme.txt
index 5f6b820..be2a63c 100644
--- a/readme.txt
+++ b/readme.txt
@@ -69,7 +69,7 @@ When the optional **group restriction** or the optional **user sync** is used, t
* `/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.
+* `/$batch` with `/users/{id}/photo`, and `/users/{id}/photos/240x240`, `/users/{id}/photo` – only while the user sync runs and "Profile photo" is selected. Returns the version and, when it changed, the image of the user's profile photo.
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.
@@ -149,6 +149,7 @@ The settings, cached data, the sync report and schedule, stored profile photos a
= 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: profile fields and photos follow Microsoft 365 on every run – changed photos are replaced, deleted photos and cleared or deselected fields are removed.
* 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.