From a0d49dc5e02e64facfa3440168413043ca047d5d Mon Sep 17 00:00:00 2001
From: Alex Morris <alex.morris@catalyst.net.nz>
Date: Fri, 21 Apr 2023 16:17:46 +1200
Subject: [PATCH 1/2] MOBILE-4214 pushnotifications: Add public key
 registration

---
 package-lock.json                             | 44 ++++++++++--------
 package.json                                  |  4 +-
 src/core/features/native/native.module.ts     |  2 +-
 .../services/pushnotifications.ts             | 46 ++++++++++++++++++-
 src/core/singletons/index.ts                  |  2 +-
 5 files changed, 75 insertions(+), 23 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index bd19b7d92..cb2ada6bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4197,21 +4197,6 @@
         }
       }
     },
-    "@ionic-native/push": {
-      "version": "5.36.0",
-      "resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-5.36.0.tgz",
-      "integrity": "sha512-N2Ei6qsIYOmqfz/kH9XpKeIp3C5Qe9NXebzH2ytkpwBApPiCc6h+9LOxgMB/rls9VfT0V0ZoxvJbac9UZ6SJmA==",
-      "requires": {
-        "@types/cordova": "^0.0.34"
-      },
-      "dependencies": {
-        "@types/cordova": {
-          "version": "0.0.34",
-          "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
-          "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA=="
-        }
-      }
-    },
     "@ionic-native/qr-scanner": {
       "version": "5.36.0",
       "resolved": "https://registry.npmjs.org/@ionic-native/qr-scanner/-/qr-scanner-5.36.0.tgz",
@@ -11588,10 +11573,33 @@
       "resolved": "https://registry.npmjs.org/@moodlehq/cordova-plugin-zip/-/cordova-plugin-zip-3.1.0-moodle.1.tgz",
       "integrity": "sha512-QD5S6bsm6awJrNMb8YN/vkYghKAMfZMHccdimx6s1i5S9fgZUSf7L477NJqjFu62imVZYJIJuavBbw5fR/562w=="
     },
+    "@moodlehq/ionic-native-push": {
+      "version": "5.36.0-moodle.2",
+      "resolved": "https://registry.npmjs.org/@moodlehq/ionic-native-push/-/ionic-native-push-5.36.0-moodle.2.tgz",
+      "integrity": "sha512-UWT4WaoTEEqGQ5pu+CyakXCOhiXsQSb8mD8j89jDqV0hJyrIQ8zA2ciGW9Y/vd55NuqjCu1tNJcWEJ4WcRVv0Q==",
+      "requires": {
+        "@angular/core": "^9.1.12",
+        "@ionic-native/core": "^5.1.0",
+        "@types/cordova": "^11.0.0",
+        "rxjs": "^5.5.0 || ^6.5.0"
+      },
+      "dependencies": {
+        "@angular/core": {
+          "version": "9.1.13",
+          "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.1.13.tgz",
+          "integrity": "sha512-mBm24Q9GjkAsxMAzqQ86U1078+yTEpr0+syMEruUtJ0HUH6Fzn3J+6xTLb+BVcGb9RkCkFaV9T5mcn6ZM0f++g=="
+        },
+        "@types/cordova": {
+          "version": "11.0.0",
+          "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-11.0.0.tgz",
+          "integrity": "sha512-AtBm1IAqqXsXszJe6XxuA2iXLhraNCj25p/FHRyikPeW0Z3YfgM6qzWb+VJglJTmZc5lqRNy84cYM/sQI5v6Vw=="
+        }
+      }
+    },
     "@moodlehq/phonegap-plugin-push": {
-      "version": "4.0.0-moodle.2",
-      "resolved": "https://registry.npmjs.org/@moodlehq/phonegap-plugin-push/-/phonegap-plugin-push-4.0.0-moodle.2.tgz",
-      "integrity": "sha512-kxHnpCzM7VMw5XUrLeZX03bLkQzA3j//+4nq7MiZbLoliPsQRAxGqyZ9HmbLcPsvlt1h7NM1eSVG52qZ7D3PlQ=="
+      "version": "4.0.0-moodle.3",
+      "resolved": "https://registry.npmjs.org/@moodlehq/phonegap-plugin-push/-/phonegap-plugin-push-4.0.0-moodle.3.tgz",
+      "integrity": "sha512-oJTmcVN6QBxo8+9uHEFTLCgNJkd7jeaT1MMM3ljDhR5EkFDkHwMMat/Km0tjm+9ToD0LYWHZfvLljpCZM5u3yg=="
     },
     "@mrmlnc/readdir-enhanced": {
       "version": "2.2.1",
diff --git a/package.json b/package.json
index 2410b387a..f58b902a1 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,6 @@
     "@ionic-native/local-notifications": "5.36.0",
     "@ionic-native/media-capture": "5.36.0",
     "@ionic-native/network": "5.36.0",
-    "@ionic-native/push": "5.36.0",
     "@ionic-native/qr-scanner": "5.36.0",
     "@ionic-native/splash-screen": "5.36.0",
     "@ionic-native/sqlite": "5.36.0",
@@ -80,7 +79,8 @@
     "@moodlehq/cordova-plugin-local-notification": "0.9.0-moodle.11",
     "@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.5",
     "@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
-    "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.2",
+    "@moodlehq/ionic-native-push": "5.36.0-moodle.2",
+    "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.3",
     "@ngx-translate/core": "13.0.0",
     "@ngx-translate/http-loader": "6.0.0",
     "@types/chart.js": "2.9.31",
diff --git a/src/core/features/native/native.module.ts b/src/core/features/native/native.module.ts
index c0a82490f..ae1248de5 100644
--- a/src/core/features/native/native.module.ts
+++ b/src/core/features/native/native.module.ts
@@ -30,7 +30,7 @@ import { WebView } from '@ionic-native/ionic-webview/ngx';
 import { Keyboard } from '@ionic-native/keyboard/ngx';
 import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
 import { MediaCapture } from '@ionic-native/media-capture/ngx';
-import { Push } from '@ionic-native/push/ngx';
+import { Push } from '@moodlehq/ionic-native-push/ngx';
 import { QRScanner } from '@ionic-native/qr-scanner/ngx';
 import { SplashScreen } from '@ionic-native/splash-screen/ngx';
 import { SQLite } from '@ionic-native/sqlite/ngx';
diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts
index 417ee3849..4d6534c83 100644
--- a/src/core/features/pushnotifications/services/pushnotifications.ts
+++ b/src/core/features/pushnotifications/services/pushnotifications.ts
@@ -14,7 +14,7 @@
 
 import { Injectable } from '@angular/core';
 import { ILocalNotification } from '@ionic-native/local-notifications';
-import { NotificationEventResponse, PushOptions, RegistrationEventResponse } from '@ionic-native/push/ngx';
+import { NotificationEventResponse, PushOptions, RegistrationEventResponse } from '@moodlehq/ionic-native-push/ngx';
 
 import { CoreApp } from '@services/app';
 import { CoreSites } from '@services/sites';
@@ -750,6 +750,33 @@ export class CorePushNotificationsProvider {
             // Remove pending unregisters for this site.
             await CoreUtils.ignoreErrors(this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() }));
         }
+
+        this.registerPublicKeyOnMoodle();
+    }
+
+    /**
+     * Register a public key on a Moodle site.
+     */
+    async registerPublicKeyOnMoodle(): Promise<void> {
+        this.logger.debug('Register public key on Moodle.');
+
+        const site = await CoreSites.getSite();
+
+        const publicKey = await Push.getPublicKey();
+        if (publicKey == null) {
+            throw new CoreError('Cannot get app public key.');
+        }
+
+        const data: CoreUserUpdateUserDevicePublicKeyWSParams = {
+            uuid: Device.uuid,
+            appid: CoreConstants.CONFIG.app_id,
+            publickey: publicKey,
+        };
+
+        await site.write<CoreUserUpdateUserDevicePublicKeyWSResponse>(
+            'core_user_update_user_device_public_key',
+            data,
+        );
     }
 
     /**
@@ -936,3 +963,20 @@ export type CoreUserAddUserDeviceWSParams = {
  * Data returned by core_user_add_user_device WS.
  */
 export type CoreUserAddUserDeviceWSResponse = CoreWSExternalWarning[][];
+
+/**
+ * Params of core_user_update_user_device_public_key WS.
+ */
+export type CoreUserUpdateUserDevicePublicKeyWSParams = {
+    uuid: string;
+    appid: string;
+    publickey: string;
+};
+
+/**
+ * Data returned by core_user_update_user_device_public_key WS.
+ */
+export type CoreUserUpdateUserDevicePublicKeyWSResponse = {
+    status: boolean;
+    warnings?: CoreWSExternalWarning[];
+};
diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts
index 60b71d369..5cad775d7 100644
--- a/src/core/singletons/index.ts
+++ b/src/core/singletons/index.ts
@@ -53,7 +53,7 @@ import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx';
 import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx';
 import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx';
 import { MediaCapture as MediaCaptureService } from '@ionic-native/media-capture/ngx';
-import { Push as PushService } from '@ionic-native/push/ngx';
+import { Push as PushService } from '@moodlehq/ionic-native-push/ngx';
 import { QRScanner as QRScannerService } from '@ionic-native/qr-scanner/ngx';
 import { StatusBar as StatusBarService } from '@ionic-native/status-bar/ngx';
 import { SplashScreen as SplashScreenService } from '@ionic-native/splash-screen/ngx';

From e56a47e35d36f16f45390acb9be18195c4105e0f Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 26 Apr 2023 16:03:16 +0200
Subject: [PATCH 2/2] MOBILE-4214 push: Improve register public key in Moodle

Now the WebService won't be called again if it has already been called successfully and public key hasn't changed
---
 jest.config.js                                |   2 +-
 .../services/database/pushnotifications.ts    |  16 +-
 .../services/pushnotifications.ts             | 161 +++++++++++-------
 3 files changed, 117 insertions(+), 62 deletions(-)

diff --git a/jest.config.js b/jest.config.js
index 45708f586..9cf9027a8 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -12,7 +12,7 @@ module.exports = {
     transform: {
         '^.+\\.(ts|html)$': 'ts-jest',
     },
-    transformIgnorePatterns: ['node_modules/(?!@ionic-native|@ionic)'],
+    transformIgnorePatterns: ['node_modules/(?!@ionic-native|@ionic|@moodlehq/ionic-native-push)'],
     moduleNameMapper: {
         ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src/' }),
         '^!raw-loader!.*': 'jest-raw-loader',
diff --git a/src/core/features/pushnotifications/services/database/pushnotifications.ts b/src/core/features/pushnotifications/services/database/pushnotifications.ts
index e362803b2..d1e32a531 100644
--- a/src/core/features/pushnotifications/services/database/pushnotifications.ts
+++ b/src/core/features/pushnotifications/services/database/pushnotifications.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { SQLiteDB } from '@classes/sqlitedb';
 import { CoreAppSchema } from '@services/app';
 import { CoreSiteSchema } from '@services/sites';
 
@@ -21,7 +22,7 @@ import { CoreSiteSchema } from '@services/sites';
  */
 export const BADGE_TABLE_NAME = 'addon_pushnotifications_badge';
 export const PENDING_UNREGISTER_TABLE_NAME = 'addon_pushnotifications_pending_unregister';
-export const REGISTERED_DEVICES_TABLE_NAME = 'addon_pushnotifications_registered_devices';
+export const REGISTERED_DEVICES_TABLE_NAME = 'addon_pushnotifications_registered_devices_2';
 export const APP_SCHEMA: CoreAppSchema = {
     name: 'CorePushNotificationsProvider',
     version: 1,
@@ -70,7 +71,7 @@ export const APP_SCHEMA: CoreAppSchema = {
 };
 export const SITE_SCHEMA: CoreSiteSchema = {
     name: 'AddonPushNotificationsProvider',
-    version: 1,
+    version: 2,
     tables: [
         {
             name: REGISTERED_DEVICES_TABLE_NAME,
@@ -103,10 +104,20 @@ export const SITE_SCHEMA: CoreSiteSchema = {
                     name: 'pushid',
                     type: 'TEXT',
                 },
+                {
+                    name: 'publickey',
+                    type: 'TEXT',
+                },
             ],
             primaryKeys: ['appid', 'uuid'],
         },
     ],
+    async migrate(db: SQLiteDB, oldVersion: number): Promise<void> {
+        if (oldVersion < 2) {
+            // Schema changed in v4.2.
+            await db.migrateTable('addon_pushnotifications_registered_devices', REGISTERED_DEVICES_TABLE_NAME);
+        }
+    },
 };
 
 /**
@@ -139,4 +150,5 @@ export type CorePushNotificationsRegisteredDeviceDBRecord = {
     platform: string; // Device platform.
     version: string; // Device version.
     pushid: string; // Push ID.
+    publickey?: string; // Public key.
 };
diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts
index 4d6534c83..3f2ee4734 100644
--- a/src/core/features/pushnotifications/services/pushnotifications.ts
+++ b/src/core/features/pushnotifications/services/pushnotifications.ts
@@ -103,10 +103,14 @@ export class CorePushNotificationsProvider {
 
         // Register device on Moodle site when login.
         CoreEvents.on(CoreEvents.LOGIN, async () => {
+            if (!this.canRegisterOnMoodle()) {
+                return;
+            }
+
             try {
                 await this.registerDeviceOnMoodle();
             } catch (error) {
-                this.logger.warn('Can\'t register device', error);
+                this.logger.error('Can\'t register device', error);
             }
         });
 
@@ -305,11 +309,11 @@ export class CorePushNotificationsProvider {
     }
 
     /**
-     * Get data to register the device in Moodle.
+     * Get required data to register the device in Moodle.
      *
      * @returns Data.
      */
-    protected getRegisterData(): CoreUserAddUserDeviceWSParams {
+    protected getRequiredRegisterData(): CoreUserAddUserDeviceWSParams {
         if (!this.pushID) {
             throw new CoreError('Cannot get register data because pushID is not set.');
         }
@@ -581,7 +585,7 @@ export class CorePushNotificationsProvider {
 
         await CoreUtils.ignoreErrors(Promise.all([
             // Remove the device from the local DB.
-            this.registeredDevicesTables[site.getId()].delete(this.getRegisterData()),
+            this.registeredDevicesTables[site.getId()].delete(this.getRequiredRegisterData()),
             // Remove pending unregisters for this site.
             this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() }),
         ]));
@@ -680,12 +684,12 @@ export class CorePushNotificationsProvider {
                 // Execute the callback in the Angular zone, so change detection doesn't stop working.
                 NgZone.run(() => {
                     this.pushID = data.registrationId;
-                    if (!CoreSites.isLoggedIn()) {
+                    if (!CoreSites.isLoggedIn() || !this.canRegisterOnMoodle()) {
                         return;
                     }
 
                     this.registerDeviceOnMoodle().catch((error) => {
-                        this.logger.warn('Can\'t register device', error);
+                        this.logger.error('Can\'t register device', error);
                     });
                 });
             });
@@ -721,62 +725,95 @@ export class CorePushNotificationsProvider {
 
         try {
 
-            const data = this.getRegisterData();
-            let result = {
-                unregister: true,
-                register: true,
-            };
+            const data = this.getRequiredRegisterData();
+            data.publickey = await this.getPublicKey(site);
 
-            if (!forceUnregister) {
-                // Check if the device is already registered.
-                result = await this.shouldRegister(data, site);
-            }
+            const neededActions = await this.getRegisterDeviceActions(data, site, forceUnregister);
 
-            if (result.unregister) {
+            if (neededActions.unregister) {
                 // Unregister the device first.
                 await CoreUtils.ignoreErrors(this.unregisterDeviceOnMoodle(site));
             }
 
-            if (result.register) {
+            if (neededActions.register) {
                 // Now register the device.
-                await site.write('core_user_add_user_device', CoreUtils.clone(data));
+                const addDeviceResponse =
+                    await site.write<CoreUserAddUserDeviceWSResponse>('core_user_add_user_device', CoreUtils.clone(data));
+
+                const deviceAlreadyRegistered =
+                    addDeviceResponse[0] && addDeviceResponse[0].find(warning => warning.warningcode === 'existingkeyforthisuser');
+                if (deviceAlreadyRegistered && data.publickey) {
+                    // Device already registered, make sure the public key is up to date.
+                    await this.updatePublicKeyOnMoodle(site, data);
+                }
 
                 CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId());
 
                 // Insert the device in the local DB.
                 await CoreUtils.ignoreErrors(this.registeredDevicesTables[site.getId()].insert(data));
+            } else if (neededActions.updatePublicKey) {
+                // Device already registered, make sure the public key is up to date.
+                const response = await this.updatePublicKeyOnMoodle(site, data);
+
+                if (response?.warnings?.find(warning => warning.warningcode === 'devicedoesnotexist')) {
+                    // The device doesn't exist in the server. Remove the device from the local DB and try again.
+                    await this.registeredDevicesTables[site.getId()].delete({
+                        appid: data.appid,
+                        uuid: data.uuid,
+                        name: data.name,
+                        model: data.model,
+                        platform: data.platform,
+                    });
+
+                    await this.registerDeviceOnMoodle(siteId, false);
+                }
             }
         } finally {
             // Remove pending unregisters for this site.
             await CoreUtils.ignoreErrors(this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() }));
         }
-
-        this.registerPublicKeyOnMoodle();
     }
 
     /**
-     * Register a public key on a Moodle site.
+     * Get the public key to register in a site.
+     *
+     * @param site Site to register
+     * @returns Public key, undefined if the site or the device doesn't support encryption.
      */
-    async registerPublicKeyOnMoodle(): Promise<void> {
-        this.logger.debug('Register public key on Moodle.');
-
-        const site = await CoreSites.getSite();
-
-        const publicKey = await Push.getPublicKey();
-        if (publicKey == null) {
-            throw new CoreError('Cannot get app public key.');
+    protected async getPublicKey(site: CoreSite): Promise<string | undefined> {
+        if (!site.isVersionGreaterEqualThan('4.2')) {
+            return;
         }
 
-        const data: CoreUserUpdateUserDevicePublicKeyWSParams = {
-            uuid: Device.uuid,
-            appid: CoreConstants.CONFIG.app_id,
-            publickey: publicKey,
+        const publicKey = await Push.getPublicKey();
+
+        return publicKey ?? undefined;
+    }
+
+    /**
+     * Update a public key on a Moodle site.
+     *
+     * @param site Site.
+     * @param data Device data.
+     * @returns WS response, undefined if no public key.
+     */
+    protected async updatePublicKeyOnMoodle(
+        site: CoreSite,
+        data: CoreUserAddUserDeviceWSParams,
+    ): Promise<CoreUserUpdateUserDevicePublicKeyWSResponse | undefined> {
+        if (!data.publickey) {
+            return;
+        }
+
+        this.logger.debug('Update public key on Moodle.');
+
+        const params: CoreUserUpdateUserDevicePublicKeyWSParams = {
+            uuid: data.uuid,
+            appid: data.appid,
+            publickey: data.publickey,
         };
 
-        await site.write<CoreUserUpdateUserDevicePublicKeyWSResponse>(
-            'core_user_update_user_device_public_key',
-            data,
-        );
+        return await site.write<CoreUserUpdateUserDevicePublicKeyWSResponse>('core_user_update_user_device_public_key', params);
     }
 
     /**
@@ -839,16 +876,26 @@ export class CorePushNotificationsProvider {
     }
 
     /**
-     * Check if device should be registered (and unregistered first).
+     * Get the needed actions to perform to register a device.
      *
      * @param data Data of the device.
      * @param site Site to use.
-     * @returns Promise resolved with booleans: whether to register/unregister.
+     * @param forceUnregister Whether to force unregister and register.
+     * @returns Whether each action needs to be performed or not.
      */
-    protected async shouldRegister(
+    protected async getRegisterDeviceActions(
         data: CoreUserAddUserDeviceWSParams,
         site: CoreSite,
-    ): Promise<{register: boolean; unregister: boolean}> {
+        forceUnregister?: boolean,
+    ): Promise<RegisterDeviceActions> {
+        if (forceUnregister) {
+            // No need to check if device is stored, always unregister and register the device.
+            return {
+                unregister: true,
+                register: true,
+                updatePublicKey: false,
+            };
+        }
 
         // Check if the device is already registered.
         const records = await CoreUtils.ignoreErrors(
@@ -863,35 +910,24 @@ export class CorePushNotificationsProvider {
 
         let isStored = false;
         let versionOrPushChanged = false;
+        let updatePublicKey = false;
 
         (records || []).forEach((record) => {
             if (record.version == data.version && record.pushid == data.pushid) {
                 // The device is already stored.
                 isStored = true;
+                updatePublicKey = !!data.publickey && record.publickey !== data.publickey;
             } else {
                 // The version or pushid has changed.
                 versionOrPushChanged = true;
             }
         });
 
-        if (isStored) {
-            // The device has already been registered, no need to register it again.
-            return {
-                register: false,
-                unregister: false,
-            };
-        } else if (versionOrPushChanged) {
-            // This data can be updated by calling register WS, no need to call unregister.
-            return {
-                register: true,
-                unregister: false,
-            };
-        } else {
-            return {
-                register: true,
-                unregister: true,
-            };
-        }
+        return {
+            register: !isStored, // No need to register if device is already stored.
+            unregister: !isStored && !versionOrPushChanged, // No need to unregister first if only version or push changed.
+            updatePublicKey,
+        };
     }
 
 }
@@ -957,6 +993,7 @@ export type CoreUserAddUserDeviceWSParams = {
     version: string; // The device version '6.1.2' or '4.2.2' etc.
     pushid: string; // The device PUSH token/key/identifier/registration id.
     uuid: string; // The device UUID.
+    publickey?: string; // @since 4.2. The app generated public key.
 };
 
 /**
@@ -980,3 +1017,9 @@ export type CoreUserUpdateUserDevicePublicKeyWSResponse = {
     status: boolean;
     warnings?: CoreWSExternalWarning[];
 };
+
+type RegisterDeviceActions = {
+    register: boolean; // Whether device needs to be registered in LMS.
+    unregister: boolean; // Whether device needs to be unregistered before register in LMS to make sure data is up to date.
+    updatePublicKey: boolean; // Whether only public key needs to be updated.
+};