From 338c2aad6817f4f8c211d7874d8029b6f9256a05 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Oct 2020 08:54:16 +0100 Subject: [PATCH 1/3] MOBILE-3565 core: Update WKUserScript plugin --- package-lock.json | 2 +- src/app/services/utils/iframe.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5adfa12d4..0ef732cd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6454,7 +6454,7 @@ "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ==" }, "cordova-plugin-wkuserscript": { - "version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#6413f4bb3c2565f353e690b5c1450b69ad9e860e", + "version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#aa77d0f98a3fb106f2e798e5adf5882f01a2c947", "from": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git" }, "cordova-plugin-wkwebview-cookies": { diff --git a/src/app/services/utils/iframe.ts b/src/app/services/utils/iframe.ts index f419f21f1..b6e09d003 100644 --- a/src/app/services/utils/iframe.ts +++ b/src/app/services/utils/iframe.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { NavController } from '@ionic/angular'; -import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript'; +import { WKUserScriptWindow } from 'cordova-plugin-wkuserscript'; import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; import { CoreApp } from '@services/app'; @@ -470,7 +470,7 @@ export class CoreIframeUtilsProvider { userScriptWindow.WKUserScript?.addScript({ id: 'CoreIframeUtilsRecaptchaScript', file: recaptchaPath, - injectionTime: WKUserScriptInjectionTime.END, + injectionTime: userScriptWindow.WKUserScript?.InjectionTime.END, }); // Handle post messages received by iframes. From 9fd6c38f2d13d25f51daa91090f8b037b7126970 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Oct 2020 09:17:36 +0100 Subject: [PATCH 2/3] MOBILE-3565 core: Fix errors when compiling in prod mode --- src/app/components/input-errors/core-input-errors.html | 10 +++++----- .../core/login/pages/email-signup/email-signup.html | 2 +- .../core/login/pages/email-signup/email-signup.page.ts | 2 +- .../pages/deviceinfo/deviceinfo.page.module.ts | 2 ++ 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/app/components/input-errors/core-input-errors.html b/src/app/components/input-errors/core-input-errors.html index 9f216a964..6c1448d3b 100644 --- a/src/app/components/input-errors/core-input-errors.html +++ b/src/app/components/input-errors/core-input-errors.html @@ -2,12 +2,12 @@
- {{errorMessages[error]}} - - {{ 'core.login.invalidvaluemax' | translate:{$a: control.errors.max.max} }} + {{errorMessages[error]}} + + {{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }} - - {{ 'core.login.invalidvaluemin' | translate:{$a: control.errors.min.min} }} + + {{ 'core.login.invalidvaluemin' | translate:{$a: control.errors!.min.min} }}
diff --git a/src/app/core/login/pages/email-signup/email-signup.html b/src/app/core/login/pages/email-signup/email-signup.html index 71287fb01..bbef473a6 100644 --- a/src/app/core/login/pages/email-signup/email-signup.html +++ b/src/app/core/login/pages/email-signup/email-signup.html @@ -148,7 +148,7 @@ - + diff --git a/src/app/core/login/pages/email-signup/email-signup.page.ts b/src/app/core/login/pages/email-signup/email-signup.page.ts index 0b5eb1103..e0bac5546 100644 --- a/src/app/core/login/pages/email-signup/email-signup.page.ts +++ b/src/app/core/login/pages/email-signup/email-signup.page.ts @@ -337,7 +337,7 @@ export class CoreLoginEmailSignupPage implements OnInit { /** * Show authentication instructions. */ - protected showAuthInstructions(): void { + showAuthInstructions(): void { CoreTextUtils.instance.viewText(Translate.instance.instant('core.login.instructions'), this.authInstructions!); } diff --git a/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts b/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts index e94de2f4c..f0727d939 100644 --- a/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts +++ b/src/app/core/settings/pages/deviceinfo/deviceinfo.page.module.ts @@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; import { CoreSettingsDeviceInfoPage } from './deviceinfo.page'; @@ -38,6 +39,7 @@ const routes: Routes = [ TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, + CorePipesModule, ], declarations: [ CoreSettingsDeviceInfoPage, From c84ffc3b6a3740d661a0723a00901edd750e98fc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Oct 2020 12:37:00 +0100 Subject: [PATCH 3/3] MOBILE-3565 login: Implement reconnect page --- src/app/app.component.ts | 9 +- src/app/core/login/login-routing.module.ts | 12 +- src/app/core/login/login.scss | 2 +- ...dule.ts => change-password.page.module.ts} | 0 .../pages/credentials/credentials.page.ts | 4 +- ....module.ts => email-signup.page.module.ts} | 0 ...e.ts => forgotten-password.page.module.ts} | 0 .../core/login/pages/reconnect/reconnect.html | 90 +++++++ .../pages/reconnect/reconnect.page.module.ts | 50 ++++ .../login/pages/reconnect/reconnect.page.ts | 245 ++++++++++++++++++ ...y.module.ts => site-policy.page.module.ts} | 0 src/app/core/login/services/helper.ts | 12 +- src/app/singletons/object.ts | 35 +++ 13 files changed, 444 insertions(+), 15 deletions(-) rename src/app/core/login/pages/change-password/{change-password.module.ts => change-password.page.module.ts} (100%) rename src/app/core/login/pages/email-signup/{email-signup.module.ts => email-signup.page.module.ts} (100%) rename src/app/core/login/pages/forgotten-password/{forgotten-password.module.ts => forgotten-password.page.module.ts} (100%) create mode 100644 src/app/core/login/pages/reconnect/reconnect.html create mode 100644 src/app/core/login/pages/reconnect/reconnect.page.module.ts create mode 100644 src/app/core/login/pages/reconnect/reconnect.page.ts rename src/app/core/login/pages/site-policy/{site-policy.module.ts => site-policy.page.module.ts} (100%) create mode 100644 src/app/singletons/object.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3296ee02d..8ddb2387a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -16,7 +16,8 @@ import { Component, OnInit } from '@angular/core'; import { NavController } from '@ionic/angular'; import { CoreLangProvider } from '@services/lang'; -import { CoreEvents } from '@singletons/events'; +import { CoreLoginHelperProvider } from '@core/login/services/helper'; +import { CoreEvents, CoreEventSessionExpiredData } from '@singletons/events'; @Component({ selector: 'app-root', @@ -28,6 +29,7 @@ export class AppComponent implements OnInit { constructor( protected langProvider: CoreLangProvider, protected navCtrl: NavController, + protected loginHelper: CoreLoginHelperProvider, ) { } @@ -46,6 +48,11 @@ export class AppComponent implements OnInit { // @todo // this.removeVersionClass(); }); + + // Listen for session expired events. + CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => { + this.loginHelper.sessionExpired(data); + }); } } diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts index b123f3b93..95ff0c8e8 100644 --- a/src/app/core/login/login-routing.module.ts +++ b/src/app/core/login/login-routing.module.ts @@ -39,21 +39,25 @@ const routes: Routes = [ }, { path: 'forgottenpassword', - loadChildren: () => import('./pages/forgotten-password/forgotten-password.module') + loadChildren: () => import('./pages/forgotten-password/forgotten-password.page.module') .then( m => m.CoreLoginForgottenPasswordPageModule), }, { path: 'changepassword', - loadChildren: () => import('./pages/change-password/change-password.module') + loadChildren: () => import('./pages/change-password/change-password.page.module') .then( m => m.CoreLoginChangePasswordPageModule), }, { path: 'sitepolicy', - loadChildren: () => import('./pages/site-policy/site-policy.module').then( m => m.CoreLoginSitePolicyPageModule), + loadChildren: () => import('./pages/site-policy/site-policy.page.module').then( m => m.CoreLoginSitePolicyPageModule), }, { path: 'emailsignup', - loadChildren: () => import('./pages/email-signup/email-signup.module').then( m => m.CoreLoginEmailSignupPageModule), + loadChildren: () => import('./pages/email-signup/email-signup.page.module').then( m => m.CoreLoginEmailSignupPageModule), + }, + { + path: 'reconnect', + loadChildren: () => import('./pages/reconnect/reconnect.page.module').then( m => m.CoreLoginReconnectPageModule), }, ]; diff --git a/src/app/core/login/login.scss b/src/app/core/login/login.scss index 6afeeed42..6e67854fa 100644 --- a/src/app/core/login/login.scss +++ b/src/app/core/login/login.scss @@ -13,7 +13,7 @@ } .core-sitename { - font-size: 1.8rem; + font-size: 1.2rem; } .core-login-site-logo { diff --git a/src/app/core/login/pages/change-password/change-password.module.ts b/src/app/core/login/pages/change-password/change-password.page.module.ts similarity index 100% rename from src/app/core/login/pages/change-password/change-password.module.ts rename to src/app/core/login/pages/change-password/change-password.page.module.ts diff --git a/src/app/core/login/pages/credentials/credentials.page.ts b/src/app/core/login/pages/credentials/credentials.page.ts index fd8bfeceb..3049a0245 100644 --- a/src/app/core/login/pages/credentials/credentials.page.ts +++ b/src/app/core/login/pages/credentials/credentials.page.ts @@ -28,7 +28,7 @@ import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes import { CoreEvents } from '@singletons/events'; /** - * Page that displays a "splash screen" while the app is being initialized. + * Page to enter the user credentials. */ @Component({ selector: 'page-core-login-credentials', @@ -161,7 +161,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { */ protected treatSiteConfig(): void { if (this.siteConfig) { - this.siteName = CoreConstants.CONFIG.sitename ?? this.siteConfig.sitename; + this.siteName = CoreConstants.CONFIG.sitename ? CoreConstants.CONFIG.sitename : this.siteConfig.sitename; this.logoUrl = CoreLoginHelper.instance.getLogoUrl(this.siteConfig); this.authInstructions = this.siteConfig.authinstructions || Translate.instance.instant('core.login.loginsteps'); diff --git a/src/app/core/login/pages/email-signup/email-signup.module.ts b/src/app/core/login/pages/email-signup/email-signup.page.module.ts similarity index 100% rename from src/app/core/login/pages/email-signup/email-signup.module.ts rename to src/app/core/login/pages/email-signup/email-signup.page.module.ts diff --git a/src/app/core/login/pages/forgotten-password/forgotten-password.module.ts b/src/app/core/login/pages/forgotten-password/forgotten-password.page.module.ts similarity index 100% rename from src/app/core/login/pages/forgotten-password/forgotten-password.module.ts rename to src/app/core/login/pages/forgotten-password/forgotten-password.page.module.ts diff --git a/src/app/core/login/pages/reconnect/reconnect.html b/src/app/core/login/pages/reconnect/reconnect.html new file mode 100644 index 000000000..add46b614 --- /dev/null +++ b/src/app/core/login/pages/reconnect/reconnect.html @@ -0,0 +1,90 @@ + + + + + + + {{ 'core.login.reconnect' | translate }} + + + +
+ + + + {{ 'core.pictureof' | translate:{$a: userFullName} }} + + + + + +
+ +
+

{{siteUrl}}

+ + +
+ + + + + + + + + + + + {{ 'core.login.cancel' | translate }} + + +
diff --git a/src/app/core/login/pages/reconnect/reconnect.page.module.ts b/src/app/core/login/pages/reconnect/reconnect.page.module.ts new file mode 100644 index 000000000..fb970ca28 --- /dev/null +++ b/src/app/core/login/pages/reconnect/reconnect.page.module.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreLoginReconnectPage } from './reconnect.page'; + +const routes: Routes = [ + { + path: '', + component: CoreLoginReconnectPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + FormsModule, + ReactiveFormsModule, + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + CoreLoginReconnectPage, + ], + exports: [RouterModule], +}) +export class CoreLoginReconnectPageModule {} diff --git a/src/app/core/login/pages/reconnect/reconnect.page.ts b/src/app/core/login/pages/reconnect/reconnect.page.ts new file mode 100644 index 000000000..3aea87e3b --- /dev/null +++ b/src/app/core/login/pages/reconnect/reconnect.page.ts @@ -0,0 +1,245 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NavController } from '@ionic/angular'; + +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreLoginHelper } from '@core/login/services/helper'; +import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site'; +import { CoreEvents } from '@singletons/events'; +import { CoreError } from '@classes/errors/error'; + +/** + * Page to enter the user password to reconnect to a site. + */ +@Component({ + selector: 'page-core-login-reconnect', + templateUrl: 'reconnect.html', + styleUrls: ['../../login.scss'], +}) +export class CoreLoginReconnectPage implements OnInit, OnDestroy { + + @ViewChild('reconnectForm') formElement?: ElementRef; + + credForm: FormGroup; + siteUrl!: string; + username!: string; + userFullName!: string; + userAvatar?: string; + siteName!: string; + logoUrl?: string; + identityProviders?: CoreSiteIdentityProvider[]; + showForgottenPassword = true; + showSiteAvatar = false; + isOAuth = false; + isLoggedOut: boolean; + siteId!: string; + + protected page?: string; + protected pageParams?: Params; + protected siteConfig?: CoreSitePublicConfigResponse; + protected viewLeft = false; + protected eventThrown = false; + + constructor( + protected navCtrl: NavController, + protected fb: FormBuilder, + protected route: ActivatedRoute, + ) { + + const currentSite = CoreSites.instance.getCurrentSite(); + + this.isLoggedOut = !!currentSite?.isLoggedOut(); + this.credForm = fb.group({ + password: ['', Validators.required], + }); + } + + /** + * Initialize the component. + */ + async ngOnInit(): Promise { + const params = this.route.snapshot.queryParams; + + this.siteId = params['siteId']; + this.page = params['pageName']; + this.pageParams = params['pageParams']; + + try { + const site = await CoreSites.instance.getSite(this.siteId); + + if (!site.infos) { + throw new CoreError('Invalid site'); + } + + this.username = site.infos.username; + this.userFullName = site.infos.fullname; + this.userAvatar = site.infos.userpictureurl; + this.siteUrl = site.infos.siteurl; + this.siteName = site.getSiteName(); + + // If login was OAuth we should only reach this page if the OAuth method ID has changed. + this.isOAuth = site.isOAuth(); + + // Show logo instead of avatar if it's a fixed site. + this.showSiteAvatar = !!this.userAvatar && !CoreLoginHelper.instance.getFixedSites(); + + const config = await CoreUtils.instance.ignoreErrors(site.getPublicConfig()); + + if (!config) { + return; + } + + this.siteConfig = config; + + await CoreSites.instance.checkRequiredMinimumVersion(config); + + // Check logoURL if user avatar is not set. + if (this.userAvatar.startsWith(this.siteUrl + '/theme/image.php')) { + this.showSiteAvatar = false; + } + this.logoUrl = CoreLoginHelper.instance.getLogoUrl(config); + + this.getDataFromConfig(this.siteConfig); + } catch (error) { + // Just leave the view. + this.cancel(); + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.viewLeft = true; + CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId); + } + + /** + * Get some data (like identity providers) from the site config. + * + * @param config Config to use. + */ + protected getDataFromConfig(config: CoreSitePublicConfigResponse): void { + const disabledFeatures = CoreLoginHelper.instance.getDisabledFeatures(config); + + this.identityProviders = CoreLoginHelper.instance.getValidIdentityProviders(config, disabledFeatures); + this.showForgottenPassword = !CoreLoginHelper.instance.isForgottenPasswordDisabled(config); + + if (!this.eventThrown && !this.viewLeft) { + this.eventThrown = true; + CoreEvents.trigger(CoreEvents.LOGIN_SITE_CHECKED, { config: config }); + } + } + + /** + * Cancel reconnect. + * + * @param e Event. + */ + cancel(e?: Event): void { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + + CoreSites.instance.logout(); + } + + /** + * Tries to authenticate the user. + * + * @param e Event. + */ + async login(e: Event): Promise { + e.preventDefault(); + e.stopPropagation(); + + CoreApp.instance.closeKeyboard(); + + // Get input data. + const password = this.credForm.value.password; + + if (!password) { + CoreDomUtils.instance.showErrorModal('core.login.passwordrequired', true); + + return; + } + + if (!CoreApp.instance.isOnline()) { + CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); + + return; + } + + const modal = await CoreDomUtils.instance.showModalLoading(); + + try { + // Start the authentication process. + const data = await CoreSites.instance.getUserToken(this.siteUrl, this.username, password); + + await CoreSites.instance.updateSiteToken(this.siteUrl, this.username, data.token, data.privateToken); + + CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); + + // Update site info too. + await CoreSites.instance.updateSiteInfoByUrl(this.siteUrl, this.username); + + // Reset fields so the data is not in the view anymore. + this.credForm.controls['password'].reset(); + + // Go to the site initial page. + await CoreLoginHelper.instance.goToSiteInitialPage({ + redirectPage: this.page, + redirectParams: this.pageParams, + }); + } catch (error) { + CoreLoginHelper.instance.treatUserTokenError(this.siteUrl, error, this.username, password); + + if (error.loggedout) { + this.cancel(); + } else if (error.errorcode == 'forcepasswordchangenotice') { + // Reset password field. + this.credForm.controls.password.reset(); + } + } finally { + modal.dismiss(); + } + } + + /** + * Forgotten password button clicked. + */ + forgottenPassword(): void { + CoreLoginHelper.instance.forgottenPasswordClicked(this.siteUrl, this.username, this.siteConfig); + } + + /** + * An OAuth button was clicked. + * + * @param provider The provider that was clicked. + */ + oauthClicked(provider: CoreSiteIdentityProvider): void { + if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig?.launchurl)) { + CoreDomUtils.instance.showErrorModal('Invalid data.'); + } + } + +} diff --git a/src/app/core/login/pages/site-policy/site-policy.module.ts b/src/app/core/login/pages/site-policy/site-policy.page.module.ts similarity index 100% rename from src/app/core/login/pages/site-policy/site-policy.module.ts rename to src/app/core/login/pages/site-policy/site-policy.page.module.ts diff --git a/src/app/core/login/services/helper.ts b/src/app/core/login/services/helper.ts index de0026857..126f29958 100644 --- a/src/app/core/login/services/helper.ts +++ b/src/app/core/login/services/helper.ts @@ -35,6 +35,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { NavigationOptions } from '@ionic/angular/providers/nav-controller'; +import { CoreObject } from '@/app/singletons/object'; /** * Helper provider that provides some common features regarding authentication. @@ -126,7 +127,7 @@ export class CoreLoginHelperProvider { const currentSite = CoreSites.instance.getCurrentSite(); const currentPage = CoreApp.instance.getCurrentPage(); - if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == 'login/reconnect') { + if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == '/login/reconnect') { // User must reauthenticate but he closed the InAppBrowser without doing so, logout him. CoreSites.instance.logout(); } @@ -1106,14 +1107,11 @@ export class CoreLoginHelperProvider { this.isOpeningReconnect = true; await CoreUtils.instance.ignoreErrors(this.navCtrl.navigateRoot('/login/reconnect', { - queryParams: { - infoSiteUrl: info.siteurl, - siteUrl: result.siteUrl, - siteId: siteId, + queryParams: CoreObject.removeUndefined({ + siteId, pageName: data.pageName, pageParams: data.params, - siteConfig: result.config, - }, + }), })); this.isOpeningReconnect = false; diff --git a/src/app/singletons/object.ts b/src/app/singletons/object.ts new file mode 100644 index 000000000..ac908aa09 --- /dev/null +++ b/src/app/singletons/object.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Singleton with helper functions for objects. + */ +export class CoreObject { + + /** + * Delete all keys from an object whose value are null or undefined. + * + * @param object Object to modify. + */ + static removeUndefined(object: T): T { + for (const name in object) { + if (object[name] === undefined) { + delete object[name]; + } + } + + return object; + } + +}