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: '/src/' }), '^!raw-loader!.*': 'jest-raw-loader', 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/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 { + 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 417ee3849..3f2ee4734 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'; @@ -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,30 +725,48 @@ 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('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. @@ -752,6 +774,48 @@ export class CorePushNotificationsProvider { } } + /** + * 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. + */ + protected async getPublicKey(site: CoreSite): Promise { + if (!site.isVersionGreaterEqualThan('4.2')) { + return; + } + + 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 { + if (!data.publickey) { + return; + } + + this.logger.debug('Update public key on Moodle.'); + + const params: CoreUserUpdateUserDevicePublicKeyWSParams = { + uuid: data.uuid, + appid: data.appid, + publickey: data.publickey, + }; + + return await site.write('core_user_update_user_device_public_key', params); + } + /** * Get the addon/site badge counter from the database. * @@ -812,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 { + 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( @@ -836,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, + }; } } @@ -930,9 +993,33 @@ 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. }; /** * 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[]; +}; + +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. +}; 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';