Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions local/o365/classes/feature/usersync/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -1310,7 +1310,8 @@ public function sync_users(array $entraidusers = [], string $bindingusernameclai
assign.assigned assigned,
assign.photoid photoid,
assign.photoupdated photoupdated,
obj.id AS objectid
obj.id AS objectid,
obj.objectid AS o365objectid
FROM {user} u
LEFT JOIN {auth_oidc_token} tok ON tok.userid = u.id
LEFT JOIN {local_o365_connections} conn ON conn.muserid = u.id
Expand Down Expand Up @@ -1389,7 +1390,8 @@ public function sync_users(array $entraidusers = [], string $bindingusernameclai
assign.assigned assigned,
assign.photoid photoid,
assign.photoupdated photoupdated,
obj.id AS objectid
obj.id AS objectid,
obj.objectid AS o365objectid
FROM {user} u
LEFT JOIN {auth_oidc_token} tok ON tok.userid = u.id
LEFT JOIN {local_o365_connections} conn ON conn.muserid = u.id
Expand Down Expand Up @@ -1913,13 +1915,22 @@ protected function sync_existing_user(
}
}

// Use pre-fetched O365 object record to avoid N+1 query problem.
$localo365objectrecord = $this->o365objectsbymoodleid[$existinguser->muserid] ?? null;
if ($localo365objectrecord && $localo365objectrecord->id == $existinguser->objectid) {
if ($localo365objectrecord->objectid != $userobjectid) {
$localo365objectrecord->objectid = $userobjectid;
$DB->update_record('local_o365_objects', $localo365objectrecord);
$this->mtrace('Updated user object ID in local_o365_object record.');
// The stored Entra ID is usually already available from the existing user query. Fall back to the
// pre-fetched object cache (no extra DB query) if this record was built without it, to detect a
// changed GUID (e.g. the Entra ID account was deleted and recreated).
if (!empty($existinguser->objectid)) {
if (isset($existinguser->o365objectid)) {
$storedobjectid = $existinguser->o365objectid;
} else {
$cachedobject = $this->o365objectsbymoodleid[$existinguser->muserid] ?? null;
$storedobjectid = $cachedobject->objectid ?? null;
}

if ($storedobjectid != $userobjectid) {
$updated = $DB->set_field('local_o365_objects', 'objectid', $userobjectid, ['id' => $existinguser->objectid]);
if ($updated) {
$this->mtrace('Updated user object ID in local_o365_objects record.');
}
}
}

Expand Down
71 changes: 71 additions & 0 deletions local/o365/classes/task/forcefullusersync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* An adhoc task to force a one-off full Microsoft Entra ID user sync.
*
* @package local_o365
* @author Lai Wei <lai.wei@enovation.ie>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright (C) 2026 onwards Microsoft, Inc. (http://microsoft.com/)
*/

namespace local_o365\task;

use core\task\adhoc_task;
use local_o365\feature\usersync\main;

/**
* Adhoc task to force a one-off full Microsoft Entra ID user sync.
*
* Queued during upgrade to repair Moodle accounts whose linked Microsoft Entra ID object ID
* went stale (e.g. the Entra ID account was deleted and recreated more than 30 days ago) before
* the fix to the object ID repair logic in the user sync task. Delta sync only reports users
* that changed since the last sync, so accounts that were already stale before this fix would
* not otherwise be picked up again on their own. If the site currently uses delta sync, this
* temporarily enables a full sync for a single run, then restores the previous setting. If a
* full sync already runs every time, no override is needed.
*/
class forcefullusersync extends adhoc_task {
/**
* Execute the task.
*
* @return bool
*/
public function execute(): bool {
if (main::sync_option_enabled('nodelta')) {
mtrace('Full sync already runs every time (nodelta enabled). No override needed.');

return true;
}

$originalsetting = (string) get_config('local_o365', 'usersync');
$originaloptions = array_filter(explode(',', $originalsetting));

mtrace('Delta sync is currently in use. Temporarily forcing a full sync for this run...');
set_config('usersync', implode(',', array_merge($originaloptions, ['nodelta'])), 'local_o365');

try {
$task = new usersync();
$task->execute();
} finally {
set_config('usersync', implode(',', $originaloptions), 'local_o365');
mtrace('Restored previous delta sync setting.');
}

return true;
}
}
17 changes: 17 additions & 0 deletions local/o365/db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -1641,5 +1641,22 @@ function xmldb_local_o365_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2025100600.03, 'local', 'o365');
}

if ($oldversion < 2025100602.04) {
// Queue adhoc task to force a one-off full user sync, repairing accounts whose linked
// Microsoft Entra ID object ID went stale (e.g. the Entra ID account was deleted and
// recreated) before the object ID repair logic in the user sync task was fixed. Delta
// sync only reports users that changed since the last sync, so already-stale accounts
// would not otherwise be picked up again on their own.
$task = new \local_o365\task\forcefullusersync();
// Set next run time to ensure it runs in the next cron cycle, not during upgrade.
$task->set_next_run_time(time() + 60);
\core\task\manager::queue_adhoc_task($task);

mtrace('Queued adhoc task to force a full user sync and repair stale Entra ID object IDs.');

// O365 savepoint reached.
upgrade_plugin_savepoint(true, 2025100602.04, 'local', 'o365');
}

return true;
}
2 changes: 1 addition & 1 deletion local/o365/version.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

defined('MOODLE_INTERNAL') || die();

$plugin->version = 2025100602;
$plugin->version = 2025100602.04;
$plugin->requires = 2025100600;
$plugin->release = '5.1.2';
$plugin->component = 'local_o365';
Expand Down