From 07168482d78ec592766d3be902067f7df3ac0c5a Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 12 Mar 2026 10:59:06 +0100 Subject: [PATCH 1/2] MDL-87795 message_airnotifier: Handle 404 (unregistered token) responses --- .upgradenotes/MDL-87795-2026021809474024.yml | 7 +++++++ .../output/airnotifier/message_output_airnotifier.php | 8 ++++++++ public/user/lib.php | 7 +++++-- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .upgradenotes/MDL-87795-2026021809474024.yml diff --git a/.upgradenotes/MDL-87795-2026021809474024.yml b/.upgradenotes/MDL-87795-2026021809474024.yml new file mode 100644 index 00000000000..f2af11ad37b --- /dev/null +++ b/.upgradenotes/MDL-87795-2026021809474024.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-87795 +notes: + core_user: + - message: >- + Added new optional parameter `userid` to the `user_remove_user_device` + function. + type: improved diff --git a/public/message/output/airnotifier/message_output_airnotifier.php b/public/message/output/airnotifier/message_output_airnotifier.php index e260a44c098..59679b24211 100644 --- a/public/message/output/airnotifier/message_output_airnotifier.php +++ b/public/message/output/airnotifier/message_output_airnotifier.php @@ -160,6 +160,14 @@ class message_output_airnotifier extends message_output { // JSON POST raw body request. $resp = $curl->post($serverurl, json_encode($params)); + + // Check if the device token is no longer registered. + if ($curl->info['http_code'] === 404) { + $json = json_decode($resp, true); + if (($json['error'] ?? null) === 'Unregistered token') { + user_remove_user_device($devicetoken->uuid, $devicetoken->appid, $devicetoken->userid); + } + } } return true; diff --git a/public/user/lib.php b/public/user/lib.php index d724d566e13..0e9dbe2edc1 100644 --- a/public/user/lib.php +++ b/public/user/lib.php @@ -1148,13 +1148,16 @@ function user_is_previously_used_password($userid, $password) { * * @param string $uuid The device UUID. * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed. + * @param int|null $userid The user id. If null, the current user will be used. * @return bool true if removed, false if the device didn't exists in the database * @since Moodle 2.9 */ -function user_remove_user_device($uuid, $appid = "") { +function user_remove_user_device($uuid, $appid = "", $userid = null) { global $DB, $USER; - $conditions = array('uuid' => $uuid, 'userid' => $USER->id); + $userid ??= $USER->id; + + $conditions = ['uuid' => $uuid, 'userid' => $userid]; if (!empty($appid)) { $conditions['appid'] = $appid; } From 00b4ad286390bc5b9f25a3329f53adaa8418958c Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Thu, 12 Mar 2026 10:59:06 +0100 Subject: [PATCH 2/2] MDL-87795 message_airnotifier: Add task to cleanup devices table --- .upgradenotes/MDL-87795-2026021809530637.yml | 8 ++ .../airnotifier/classes/task/cleanup_task.php | 51 +++++++++++ .../message/output/airnotifier/db/tasks.php | 37 ++++++++ .../lang/en/message_airnotifier.php | 1 + .../tests/task/cleanup_task_test.php | 87 +++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 .upgradenotes/MDL-87795-2026021809530637.yml create mode 100644 public/message/output/airnotifier/classes/task/cleanup_task.php create mode 100644 public/message/output/airnotifier/db/tasks.php create mode 100644 public/message/output/airnotifier/tests/task/cleanup_task_test.php diff --git a/.upgradenotes/MDL-87795-2026021809530637.yml b/.upgradenotes/MDL-87795-2026021809530637.yml new file mode 100644 index 00000000000..159508a7eb1 --- /dev/null +++ b/.upgradenotes/MDL-87795-2026021809530637.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-87795 +notes: + message_airnotifier: + - message: >- + A new scheduled task, `message_airnotifier\task\cleanup_task`, has been + added. This task removes orphaned records in the + `message_airnotifier_devices` table. + type: improved diff --git a/public/message/output/airnotifier/classes/task/cleanup_task.php b/public/message/output/airnotifier/classes/task/cleanup_task.php new file mode 100644 index 00000000000..1503e642509 --- /dev/null +++ b/public/message/output/airnotifier/classes/task/cleanup_task.php @@ -0,0 +1,51 @@ +. + +namespace message_airnotifier\task; + +use core\task\scheduled_task; + +/** + * Scheduled task that cleans up orphaned Airnotifier devices. + * + * @package message_airnotifier + * @copyright 2026 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cleanup_task extends scheduled_task { + /** + * {@inheritDoc} + */ + public function get_name(): string { + return get_string('taskcleanup', 'message_airnotifier'); + } + + /** + * {@inheritDoc} + */ + public function execute(): void { + global $DB; + + $DB->delete_records_select( + 'message_airnotifier_devices', + 'NOT EXISTS ( + SELECT 1 + FROM {user_devices} + WHERE {user_devices}.id = {message_airnotifier_devices}.userdeviceid + )' + ); + } +} diff --git a/public/message/output/airnotifier/db/tasks.php b/public/message/output/airnotifier/db/tasks.php new file mode 100644 index 00000000000..e656f6e59e7 --- /dev/null +++ b/public/message/output/airnotifier/db/tasks.php @@ -0,0 +1,37 @@ +. + +/** + * This file defines tasks performed by the plugin. + * + * @package message_airnotifier + * @copyright 2026 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = [ + [ + 'classname' => 'message_airnotifier\task\cleanup_task', + 'blocking' => 0, + 'minute' => 'R', + 'hour' => 'R', + 'day' => 'R', + 'dayofweek' => '*', + 'month' => '*', + ], +]; diff --git a/public/message/output/airnotifier/lang/en/message_airnotifier.php b/public/message/output/airnotifier/lang/en/message_airnotifier.php index 739254170dc..1670cc66704 100644 --- a/public/message/output/airnotifier/lang/en/message_airnotifier.php +++ b/public/message/output/airnotifier/lang/en/message_airnotifier.php @@ -80,6 +80,7 @@ $string['sendnotificationnotenc'] = 'Send notifications without encryption'; $string['sendtest'] = 'Send test push notification to my devices'; $string['sendtestconfirmation'] = 'A test push notification will be sent to the devices you use to connect to this site. Please ensure that your devices are connected to the Internet and that the mobile app is not open (since push notifications are only displayed when received in the background).'; $string['serverconnectivityerror'] = 'This site is not able to connect to the notifications server {$a}'; +$string['taskcleanup'] = 'Clean up Airnotifier devices.'; $string['unknowndevice'] = 'Unknown device'; $string['userdevices'] = 'User devices'; $string['airnotifier:managedevice'] = 'Manage devices'; diff --git a/public/message/output/airnotifier/tests/task/cleanup_task_test.php b/public/message/output/airnotifier/tests/task/cleanup_task_test.php new file mode 100644 index 00000000000..b996e8a2a25 --- /dev/null +++ b/public/message/output/airnotifier/tests/task/cleanup_task_test.php @@ -0,0 +1,87 @@ +. + +namespace message_airnotifier\task; + +/** + * Test the functionality provided by the cleanup task. + * + * @package message_airnotifier + * @category test + * @copyright 2026 Moodle Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[\PHPUnit\Framework\Attributes\CoversClass(cleanup_task::class)] +final class cleanup_task_test extends \advanced_testcase { + /** + * Test that the cleanup task correctly removes orphaned Airnotifier devices. + */ + public function test_execute(): void { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $generator = $this->getDataGenerator(); + + // Add some user devices and Airnotifier devices. + $user = $generator->create_user(); + $userdeviceid1 = $DB->insert_record('user_devices', [ + 'appid' => 'com.moodle.moodlemobile', + 'name' => 'occam', + 'model' => 'Nexus 4', + 'platform' => 'Android', + 'version' => '4.2.2', + 'pushid' => 'apushdkasdfj4835', + 'uuid' => 'asdnfl348qlksfaasef859', + 'userid' => $user->id, + 'timecreated' => time(), + 'timemodified' => time(), + ]); + $userdeviceid2 = $DB->insert_record('user_devices', [ + 'appid' => 'com.moodle.moodlemobile', + 'name' => 'occam', + 'model' => 'Nexus 4', + 'platform' => 'Android', + 'version' => '4.2.2', + 'pushid' => 'dvcp4fkrdslv5454', + 'uuid' => 'dsvaxnc0p43rgndf4rvfdnm993', + 'userid' => $user->id, + 'timecreated' => time(), + 'timemodified' => time(), + ]); + $DB->insert_record('message_airnotifier_devices', [ + 'userdeviceid' => $userdeviceid1, + 'enable' => 1, + ]); + $DB->insert_record('message_airnotifier_devices', [ + 'userdeviceid' => $userdeviceid2, + 'enable' => 1, + ]); + + // Simulate user device deleted. + $DB->delete_records('user_devices', ['id' => $userdeviceid1]); + + // Execute the task. + \core\cron::setup_user(); + $task = new cleanup_task(); + $task->execute(); + + // Assert that the orphaned Airnotifier device has been deleted and the valid one still exists. + self::assertFalse($DB->record_exists('message_airnotifier_devices', ['userdeviceid' => $userdeviceid1])); + self::assertTrue($DB->record_exists('message_airnotifier_devices', ['userdeviceid' => $userdeviceid2])); + } +}