diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 10cfaa120..9d2b521c1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,21 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; import { Platform } from 'ionic-angular'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; @Component({ - templateUrl: 'app.html' + templateUrl: 'app.html' }) export class MyApp { - rootPage:any = 'InitPage'; + rootPage:any = 'CoreLoginInitPage'; - constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) { - platform.ready().then(() => { - // Okay, so the platform is ready and our plugins are available. - // Here you can do any higher level native things you might need. - statusBar.styleDefault(); - splashScreen.hide(); - }); - } + constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) { + platform.ready().then(() => { + // Okay, so the platform is ready and our plugins are available. + // Here you can do any higher level native things you might need. + statusBar.styleDefault(); + splashScreen.hide(); + }); + } } diff --git a/src/app/app.scss b/src/app/app.scss index 1392a6e29..fe1b0ae15 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -14,3 +14,42 @@ // To declare rules for a specific mode, create a child rule // for the .md, .ios, or .wp mode classes. The mode class is // automatically applied to the element in the app. + + +/** + * App Branding + */ +.mm-bglogo { + background-color: $mm-color-init-screen; /* Change this to add a bg image or change color */ + background: -webkit-radial-gradient($mm-color-init-screen-alt, $mm-color-init-screen); + background: radial-gradient($mm-color-init-screen-alt, $mm-color-init-screen); + background-repeat: no-repeat; + background-position: center center; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + height: 100%; + width: 100%; + display: table; + + .mm-logo { + display: table-cell; + text-align: center; + vertical-align: middle; + } + + img { + width: $mm-init-screen-logo-width; + max-width: $mm-init-screen-logo-max-width; + display: block; + margin: 0 auto; + margin-bottom: 30px; + } + + .spinner circle { + stroke: $mm-init-screen-spinner-color; + } +} + diff --git a/src/assets/img/logo_white.png b/src/assets/img/logo_white.png new file mode 100644 index 000000000..280aa87f0 Binary files /dev/null and b/src/assets/img/logo_white.png differ diff --git a/src/core/constants.ts b/src/core/constants.ts index 339726fdc..08b8d83e8 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -23,6 +23,7 @@ export class CoreConstants { public static wifiDownloadThreshold = 104857600; // 100MB. public static downloadThreshold = 10485760; // 10MB. public static dontShowError = 'CoreDontShowError'; + public static noSiteId = 'NoSite'; // Settings constants. public static settingsRichTextEditor = 'CoreSettingsRichTextEditor'; diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index 9b35d5d43..ff665cdda 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -32,7 +32,7 @@ import { NetworkMock } from './providers/network'; import { ZipMock } from './providers/zip'; import { InAppBrowser } from '@ionic-native/in-app-browser'; -import { CoreEmulatorHelper } from './providers/helper'; +import { CoreEmulatorHelperProvider } from './providers/helper'; import { CoreAppProvider } from '../../providers/app'; import { CoreFileProvider } from '../../providers/file'; import { CoreTextUtilsProvider } from '../../providers/utils/text'; @@ -46,7 +46,7 @@ import { CoreInitDelegate } from '../../providers/init'; imports: [ ], providers: [ - CoreEmulatorHelper, + CoreEmulatorHelperProvider, { provide: Clipboard, deps: [CoreAppProvider], @@ -105,7 +105,7 @@ import { CoreInitDelegate } from '../../providers/init'; ] }) export class CoreEmulatorModule { - constructor(appProvider: CoreAppProvider, initDelegate: CoreInitDelegate, helper: CoreEmulatorHelper) { + constructor(appProvider: CoreAppProvider, initDelegate: CoreInitDelegate, helper: CoreEmulatorHelperProvider) { let win = window; // Convert the "window" to "any" type to be able to use non-standard properties. // Emulate Custom URL Scheme plugin in desktop apps. diff --git a/src/core/emulator/providers/helper.ts b/src/core/emulator/providers/helper.ts index 987835a22..3c333b25d 100644 --- a/src/core/emulator/providers/helper.ts +++ b/src/core/emulator/providers/helper.ts @@ -25,7 +25,7 @@ import { FileTransferErrorMock } from './file-transfer'; * Emulates the Cordova Zip plugin in desktop apps and in browser. */ @Injectable() -export class CoreEmulatorHelper implements CoreInitHandler { +export class CoreEmulatorHelperProvider implements CoreInitHandler { name = 'CoreEmulator'; priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 500; blocking = true; diff --git a/src/core/login/login.module.ts b/src/core/login/login.module.ts new file mode 100644 index 000000000..e13f72e96 --- /dev/null +++ b/src/core/login/login.module.ts @@ -0,0 +1,27 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreLoginHelperProvider } from './providers/helper'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + CoreLoginHelperProvider, + ] +}) +export class CoreLoginModule {} diff --git a/src/core/login/pages/init/init.html b/src/core/login/pages/init/init.html index 38b1e2347..8961c1b4e 100644 --- a/src/core/login/pages/init/init.html +++ b/src/core/login/pages/init/init.html @@ -1,18 +1,8 @@ - - - - - Init - - - - - - - + + diff --git a/src/core/login/pages/init/init.module.ts b/src/core/login/pages/init/init.module.ts index 38e4ac377..ee877d5ab 100644 --- a/src/core/login/pages/init/init.module.ts +++ b/src/core/login/pages/init/init.module.ts @@ -1,13 +1,29 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; -import { InitPage } from './init'; +import { CoreLoginInitPage } from './init'; +import { CoreLoginModule } from '../../login.module'; @NgModule({ - declarations: [ - InitPage, - ], - imports: [ - IonicPageModule.forChild(InitPage), - ], + declarations: [ + CoreLoginInitPage, + ], + imports: [ + CoreLoginModule, + IonicPageModule.forChild(CoreLoginInitPage), + ], }) -export class InitPageModule {} +export class CoreLoginInitPageModule {} diff --git a/src/core/login/pages/init/init.scss b/src/core/login/pages/init/init.scss index c3cc06952..f9a6bc26e 100644 --- a/src/core/login/pages/init/init.scss +++ b/src/core/login/pages/init/init.scss @@ -1,3 +1,3 @@ -page-init { +page-core-login-init { } diff --git a/src/core/login/pages/init/init.ts b/src/core/login/pages/init/init.ts index be5ed2e27..3f12be846 100644 --- a/src/core/login/pages/init/init.ts +++ b/src/core/login/pages/init/init.ts @@ -1,25 +1,87 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 } from '@angular/core'; -import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { IonicPage, NavController } from 'ionic-angular'; +import { CoreAppProvider } from '../../../../providers/app'; +import { CoreInitDelegate } from '../../../../providers/init'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreConstants } from '../../../constants'; +import { CoreLoginHelperProvider } from '../../providers/helper'; /** - * Generated class for the InitPage page. - * - * See https://ionicframework.com/docs/components/#navigation for more info on - * Ionic pages and navigation. + * Page that displays a "splash screen" while the app is being initialized. */ - @IonicPage() @Component({ - selector: 'page-init', - templateUrl: 'init.html', + selector: 'page-core-login-init', + templateUrl: 'init.html', }) -export class InitPage { +export class CoreLoginInitPage { - constructor(public navCtrl: NavController, public navParams: NavParams) { - } + constructor(private navCtrl: NavController, private appProvider: CoreAppProvider, private initDelegate: CoreInitDelegate, + private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider) {} - ionViewDidLoad() { - console.log('ionViewDidLoad InitPage'); - } + /** + * View loaded. + */ + ionViewDidLoad() { + // Wait for the app to be ready. + this.initDelegate.ready().then(() => { + // Check if there was a pending redirect. + const redirectData = this.appProvider.getRedirect(); + if (redirectData.siteId && redirectData.page) { + // Unset redirect data. + this.appProvider.storeRedirect('', '', ''); + // Only accept the redirect if it was stored less than 20 seconds ago. + if (Date.now() - redirectData.timemodified < 20000) { + if (redirectData.siteId != CoreConstants.noSiteId) { + // The redirect is pointing to a site, load it. + return this.sitesProvider.loadSite(redirectData.siteId).then(() => { + if (!this.loginHelper.isSiteLoggedOut(redirectData.page, redirectData.params)) { + this.navCtrl.setRoot(redirectData.page, redirectData.params, {animate: false}); + } + }).catch(() => { + // Site doesn't exist. + this.loadPage(); + }); + } else { + // No site to load, just open the state. + return this.navCtrl.setRoot(redirectData.page, redirectData.params, {animate: false}); + } + } + } + + this.loadPage(); + }); + } + + /** + * Load the right page. + */ + protected loadPage() : void { + if (this.sitesProvider.isLoggedIn()) { + if (!this.loginHelper.isSiteLoggedOut()) { + this.loginHelper.goToSiteInitialPage(this.navCtrl, true); + } + } else { + this.sitesProvider.hasSites().then(() => { + this.navCtrl.setRoot('CoreLoginSitesPage'); + }, () => { + this.loginHelper.goToAddSite(this.navCtrl, true); + }); + } + } } diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts new file mode 100644 index 000000000..7f5aecf20 --- /dev/null +++ b/src/core/login/providers/helper.ts @@ -0,0 +1,700 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '../../../providers/app'; +import { CoreConfigProvider } from '../../../providers/config'; +import { CoreEventsProvider } from '../../../providers/events'; +import { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreWSProvider } from '../../../providers/ws'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../../providers/utils/text'; +import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreUtilsProvider } from '../../../providers/utils/utils'; +import { CoreConfigConstants } from '../../../configconstants'; +import { CoreConstants } from '../../constants'; +import { CoreEmulatorHelperProvider } from '../../emulator/providers/helper'; +import { Md5 } from 'ts-md5/dist/md5'; + +export interface CoreLoginSSOData { + siteUrl?: string; + token?: string; + privateToken?: string; + pageName?: string; + pageParams?: any +}; + +/** + * Emulates the Cordova Zip plugin in desktop apps and in browser. + */ +@Injectable() +export class CoreLoginHelperProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, + private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, + private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, + private urlUtils: CoreUrlUtilsProvider,private configProvider: CoreConfigProvider, + private emulatorHelper: CoreEmulatorHelperProvider) { + this.logger = logger.getInstance('CoreLoginHelper'); + } + + /** + * Accept site policy. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected if failure. + */ + acceptSitePolicy(siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.write('core_user_agree_site_policy', {}).then((result) => { + if (!result.status) { + // Error. + if (result.warnings && result.warnings.length) { + return Promise.reject(result.warnings[0].message); + } else { + return Promise.reject(null); + } + } + }); + }); + } + + /** + * Check if a site allows requesting a password reset through the app. + * + * @param {string} siteUrl URL of the site. + * @return {Promise} Promise resolved with boolean: whether can be done through the app. + */ + canRequestPasswordReset(siteUrl: string) : Promise { + return this.requestPasswordReset(siteUrl).then(() => { + return true; + }).catch((error) => { + return error.available == 1 || error.errorcode != 'invalidrecord'; + }); + } + + /** + * Show a confirm modal if needed and open a browser to perform SSO login. + * + * @param {string} siteurl URL of the site where the SSO login will be performed. + * @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode. + * @param {string} [service] The service to use. If not defined, external service will be used. + * @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used. + * @return {Void} + */ + confirmAndOpenBrowserForSSOLogin(siteUrl: string, typeOfLogin: number, service?: string, launchUrl?: string) : void { + // Show confirm only if it's needed. Treat "false" (string) as false to prevent typing errors. + let showConfirmation = this.shouldShowSSOConfirm(typeOfLogin), + promise; + + if (showConfirmation) { + promise = this.domUtils.showConfirm(this.translate.instant('mm.login.logininsiterequired')); + } else { + promise = Promise.resolve(); + } + + promise.then(() => { + this.openBrowserForSSOLogin(siteUrl, typeOfLogin, service, launchUrl); + }); + } + + /** + * Format profile fields, filtering the ones that shouldn't be shown on signup and classifying them in categories. + * + * @param {any[]} profileFields Profile fields to format. + * @return {any} Categories with the fields to show in each one. + */ + formatProfileFieldsForSignup(profileFields: any[]) : any { + let categories = {}; + + profileFields.forEach((field) => { + if (!field.signup) { + // Not a signup field, ignore it. + return; + } + + if (!categories[field.categoryid]) { + categories[field.categoryid] = { + id: field.categoryid, + name: field.categoryname, + fields: [] + } + } + + categories[field.categoryid].fields.push(field); + }); + + return categories; + } + + /** + * Builds an object with error messages for some common errors. + * Please notice that this function doesn't support all possible error types. + * + * @param {string} [requiredMsg] Code of the string for required error. + * @param {string} [emailMsg] Code of the string for invalid email error. + * @param {string} [patternMsg] Code of the string for pattern not match error. + * @param {string} [urlMsg] Code of the string for invalid url error. + * @param {string} [minlengthMsg] Code of the string for "too short" error. + * @param {string} [maxlengthMsg] Code of the string for "too long" error. + * @param {string} [minMsg] Code of the string for min value error. + * @param {string} [maxMsg] Code of the string for max value error. + * @return {any} Object with the errors. + */ + getErrorMessages(requiredMsg?: string, emailMsg?: string, patternMsg?: string, urlMsg?: string, minlengthMsg?: string, + maxlengthMsg?: string, minMsg?: string, maxMsg?: string) : any { + var errors: any = {}; + + if (requiredMsg) { + errors.required = this.translate.instant(requiredMsg); + } + if (emailMsg) { + errors.email = this.translate.instant(emailMsg); + } + if (patternMsg) { + errors.pattern = this.translate.instant(patternMsg); + } + if (urlMsg) { + errors.url = this.translate.instant(urlMsg); + } + if (minlengthMsg) { + errors.minlength = this.translate.instant(minlengthMsg); + } + if (maxlengthMsg) { + errors.maxlength = this.translate.instant(maxlengthMsg); + } + if (minMsg) { + errors.min = this.translate.instant(minMsg); + } + if (maxMsg) { + errors.max = this.translate.instant(maxMsg); + } + + return errors; + } + + /** + * Get the site policy. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the site policy. + */ + getSitePolicy(siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Check if it's stored in the site config. + let sitePolicy = site.getStoredConfig('sitepolicy'); + if (typeof sitePolicy != 'undefined') { + return sitePolicy ? sitePolicy : Promise.reject(null); + } + + // Not in the config, try to get it using auth_email_get_signup_settings. + return this.wsProvider.callAjax('auth_email_get_signup_settings', {}, {siteUrl: site.getURL()}).then((settings) => { + return settings.sitepolicy ? settings.sitepolicy : Promise.reject(null); + }); + }); + } + + /** + * Get fixed site or sites. + * + * @return {string|any[]} Fixed site or list of fixed sites. + */ + getFixedSites() : string|any[] { + return CoreConfigConstants.siteurl; + } + + /** + * Get the valid identity providers from a site config. + * + * @param {any} siteConfig Site's public config. + * @return {any[]} Valid identity providers. + */ + getValidIdentityProviders(siteConfig: any) : any[] { + let validProviders = [], + httpUrl = this.textUtils.concatenatePaths(siteConfig.wwwroot, 'auth/oauth2/'), + httpsUrl = this.textUtils.concatenatePaths(siteConfig.httpswwwroot, 'auth/oauth2/'); + + if (siteConfig.identityproviders && siteConfig.identityproviders.length) { + siteConfig.identityproviders.forEach((provider) => { + if (provider.url && (provider.url.indexOf(httpsUrl) != -1 || provider.url.indexOf(httpUrl) != -1)) { + validProviders.push(provider); + } + }); + } + + return validProviders; + } + + /** + * Go to the page to add a new site. + * If a fixed URL is configured, go to credentials instead. + * + * @param {NavController} navCtrl The NavController instance to use. + * @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack. + * @return {Promise} Promise resolved when done. + */ + goToAddSite(navCtrl: NavController, setRoot?: boolean) : Promise { + let pageName, + params; + + if (this.isFixedUrlSet()) { + // Fixed URL is set, go to credentials page. + let url = typeof CoreConfigConstants.siteurl == 'string' ? + CoreConfigConstants.siteurl : CoreConfigConstants.siteurl[0].url; + + pageName = 'CoreLoginCredentialsPage'; + params = {siteUrl: url}; + } else { + pageName = 'CoreLoginSitePage'; + } + + if (setRoot) { + return navCtrl.setRoot(pageName, params, {animate: false}); + } else { + return navCtrl.push(pageName, params); + } + } + + /** + * Go to the initial page of a site depending on 'userhomepage' setting. + * + * @param {NavController} navCtrl The NavController instance to use. + * @param {boolean} [setRoot] True to set the new page as root, false to add it to the stack. + * @return {Promise} Promise resolved when done. + */ + goToSiteInitialPage(navCtrl: NavController, setRoot?: boolean) : Promise { + return this.isMyOverviewEnabled().then((myOverview) => { + let myCourses = !myOverview && this.isMyCoursesEnabled(), + site = this.sitesProvider.getCurrentSite(), + promise; + + if (!site) { + return Promise.reject(null); + } + + // Check if frontpage is needed to be shown. (If configured or if any of the other avalaible). + if ((site.getInfo() && site.getInfo().userhomepage === 0) || (!myCourses && !myOverview)) { + promise = this.isFrontpageEnabled(); + } else { + promise = Promise.resolve(false); + } + + return promise.then((frontpage) => { + // Check avalaibility in priority order. + let pageName, + params; + + // @todo Use real pages names when they are implemented. + if (frontpage) { + pageName = 'Frontpage'; + } else if (myOverview) { + pageName = 'MyOverview'; + } else if (myCourses) { + pageName = 'MyCourses'; + } else { + // Anything else available, go to the user profile. + pageName = 'User'; + params = { + userId: site.getUserId() + }; + } + + if (setRoot) { + return navCtrl.setRoot(pageName, params, {animate: false}); + } else { + return navCtrl.push(pageName, params); + } + }); + }); + } + + /** + * Convenient helper to handle authentication in the app using a token received by SSO login. If it's a new account, + * the site is stored and the user is authenticated. If the account already exists, update its token. + * + * @param {string} siteUrl Site's URL. + * @param {string} token User's token. + * @param {string} [privateToken] User's private token. + * @return {Promise} Promise resolved when the user is authenticated with the token. + */ + handleSSOLoginAuthentication(siteUrl: string, token: string, privateToken?: string) : Promise { + if (this.sitesProvider.isLoggedIn()) { + // User logged in, he is reconnecting. Retrieve username. + var info = this.sitesProvider.getCurrentSite().getInfo(); + if (typeof info != 'undefined' && typeof info.username != 'undefined') { + return this.sitesProvider.updateSiteToken(info.siteurl, info.username, token, privateToken).then(() => { + this.sitesProvider.updateSiteInfoByUrl(info.siteurl, info.username); + }).catch(() => { + // Error updating token, return proper error message. + return Promise.reject(this.translate.instant('mm.login.errorupdatesite')); + }); + } + return Promise.reject(this.translate.instant('mm.login.errorupdatesite')); + } else { + return this.sitesProvider.newSite(siteUrl, token, privateToken); + } + } + + /** + * Check if the app is configured to use several fixed URLs. + * + * @return {boolean} Whether there are several fixed URLs. + */ + hasSeveralFixedSites() : boolean { + return CoreConfigConstants.siteurl && Array.isArray(CoreConfigConstants.siteurl) && + CoreConfigConstants.siteurl.length > 1; + } + + /** + * Given a site public config, check if email signup is disabled. + * + * @param {any} config Site public config. + * @return {boolean} Whether email signup is disabled. + */ + isEmailSignupDisabled(config: any) : boolean { + let disabledFeatures = config && config.tool_mobile_disabledfeatures; + if (!disabledFeatures) { + return false; + } + + let regEx = new RegExp('(,|^)\\$mmLoginEmailSignup(,|$)', 'g'); + return !!disabledFeatures.match(regEx); + } + + /** + * Check if the app is configured to use a fixed URL (only 1). + * + * @return {boolean} Whether there is 1 fixed URL. + */ + isFixedUrlSet() : boolean { + if (Array.isArray(CoreConfigConstants.siteurl)) { + return CoreConfigConstants.siteurl.length == 1; + } + return !!CoreConfigConstants.siteurl; + } + + /** + * Check if the app is configured to use a fixed URL (only 1). + * + * @return {Promise} Promise resolved with boolean: whether there is 1 fixed URL. + */ + protected isFrontpageEnabled() : Promise { + // var $mmaFrontpage = $mmAddonManager.get('$mmaFrontpage'); + // if ($mmaFrontpage && !$mmaFrontpage.isDisabledInSite()) { + // return $mmaFrontpage.isFrontpageAvailable().then(() => { + // return true; + // }).catch(() => { + // return false; + // }); + // } + // @todo: Implement it when front page is implemented. + return Promise.resolve(false); + } + + /** + * Check if My Courses is enabled. + * + * @return {boolean} Whether My Courses is enabled. + */ + protected isMyCoursesEnabled() : boolean { + // @todo: Implement it when My Courses is implemented. + return false; + // return !$mmCourses.isMyCoursesDisabledInSite(); + } + + /** + * Check if My Overview is enabled. + * + * @return {Promise} Promise resolved with boolean: whether My Overview is enabled. + */ + protected isMyOverviewEnabled() : Promise { + // @todo: Implement it when My Overview is implemented. + return Promise.resolve(false); + } + + /** + * Check if current site is logged out, triggering mmCoreEventSessionExpired if it is. + * + * @param {string} [pageName] Name of the page to go once authenticated if logged out. If not defined, site initial page. + * @param {any} [params] Params of the page to go once authenticated if logged out. + * @return {boolean} True if user is logged out, false otherwise. + */ + isSiteLoggedOut(pageName?: string, params?: any) : boolean { + let site = this.sitesProvider.getCurrentSite(); + if (!site) { + return false; + } + + if (site.isLoggedOut()) { + this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, { + siteId: site.getId(), + pageName: pageName, + params: params + }); + return true; + } + return false; + } + + /** + * Check if SSO login should use an embedded browser. + * + * @param {number} code Code to check. + * @return {boolean} True if embedded browser, false othwerise. + */ + isSSOEmbeddedBrowser(code: number) : boolean { + if (this.appProvider.isDesktop() && this.emulatorHelper.isLinux()) { + // In Linux desktop apps, always use embedded browser. + return true; + } + + return code == CoreConstants.loginSSOInAppCode; + } + + /** + * Check if SSO login is needed based on code returned by the WS. + * + * @param {number} code Code to check. + * @return {boolean} True if SSO login is needed, false othwerise. + */ + isSSOLoginNeeded(code: number) : boolean { + return code == CoreConstants.loginSSOCode || code == CoreConstants.loginSSOInAppCode; + } + + /** + * Open a browser to perform OAuth login (Google, Facebook, Microsoft). + * + * @param {string} siteUrl URL of the site where the login will be performed. + * @param {any} provider The identity provider. + * @param {string} [launchUrl] The URL to open for SSO. If not defined, tool/mobile launch URL will be used. + * @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page. + * @param {any} [pageParams] Params of the state to go once authenticated. + * @return {boolean} True if success, false if error. + */ + openBrowserForOAuthLogin(siteUrl: string, provider: any, launchUrl?: string, pageName?: string, pageParams?: any) : boolean { + launchUrl = launchUrl || siteUrl + '/admin/tool/mobile/launch.php'; + if (!provider || !provider.url) { + return false; + } + + let service = this.sitesProvider.determineService(siteUrl), + loginUrl = this.prepareForSSOLogin(siteUrl, service, launchUrl, pageName, pageParams), + params = this.urlUtils.extractUrlParams(provider.url); + + if (!params.id) { + return false; + } + + loginUrl += '&oauthsso=' + params.id; + + if (this.appProvider.isDesktop() && this.emulatorHelper.isLinux()) { + // In Linux desktop apps, always use embedded browser. + this.utils.openInApp(loginUrl); + } else { + // Always open it in browser because the user might have the session stored in there. + this.utils.openInBrowser(loginUrl); + if ((navigator).app) { + (navigator).app.exitApp(); + } + } + + return true; + } + + /** + * Open a browser to perform SSO login. + * + * @param {string} siteurl URL of the site where the SSO login will be performed. + * @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode. + * @param {string} [service] The service to use. If not defined, external service will be used. + * @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used. + * @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page. + * @param {any} [pageParams] Params of the state to go once authenticated. + */ + openBrowserForSSOLogin(siteUrl: string, typeOfLogin: number, service?: string, launchUrl?: string, pageName?: string, + pageParams?: any) : void { + let loginUrl = this.prepareForSSOLogin(siteUrl, service, launchUrl, pageName, pageParams); + + if (this.isSSOEmbeddedBrowser(typeOfLogin)) { + let options = { + clearsessioncache: 'yes', // Clear the session cache to allow for multiple logins. + closebuttoncaption: this.translate.instant('mm.login.cancel'), + } + this.utils.openInApp(loginUrl, options); + } else { + this.utils.openInBrowser(loginUrl); + if ((navigator).app) { + (navigator).app.exitApp(); + } + } + } + + /** + * Convenient helper to open change password page. + * + * @param {string} siteUrl Site URL to construct change password URL. + * @param {string} error Error message. + */ + openChangePassword(siteUrl: string, error: string) : void { + let alert = this.domUtils.showAlert(this.translate.instant('mm.core.notice'), error, null, 3000); + alert.onDidDismiss(() => { + this.utils.openInApp(siteUrl + '/login/change_password.php'); + }); + } + + /** + * Open forgotten password in inappbrowser. + * + * @param {string} siteUrl URL of the site. + */ + openForgottenPassword(siteUrl: string) : void { + this.utils.openInApp(siteUrl + '/login/forgot_password.php'); + } + + /** + * Prepare the app to perform SSO login. + * + * @param {string} siteUrl URL of the site where the SSO login will be performed. + * @param {string} [service] The service to use. If not defined, external service will be used. + * @param {string} [launchUrl] The URL to open for SSO. If not defined, local_mobile launch URL will be used. + * @param {string} [pageName] Name of the page to go once authenticated. If not defined, site initial page. + * @param {any} [pageParams] Params of the state to go once authenticated. + */ + prepareForSSOLogin(siteUrl: string, service?: string, launchUrl?: string, pageName?: string, pageParams?: any) : string { + service = service || CoreConfigConstants.wsextservice; + launchUrl = launchUrl || siteUrl + '/local/mobile/launch.php'; + + let passport = Math.random() * 1000, + loginUrl = launchUrl + '?service=' + service; + + loginUrl += "&passport=" + passport; + loginUrl += "&urlscheme=" + CoreConfigConstants.customurlscheme; + + // Store the siteurl and passport in $mmConfig for persistence. We are "configuring" + // the app to wait for an SSO. $mmConfig shouldn't be used as a temporary storage. + this.configProvider.set(CoreConstants.loginLaunchData, JSON.stringify({ + siteUrl: siteUrl, + passport: passport, + pageName: pageName || '', + pageParams: pageParams || {} + })); + + return loginUrl; + } + + /** + * Request a password reset. + * + * @param {string} siteUrl URL of the site. + * @param {string} [username] Username to search. + * @param {string} [email] Email to search. + * @return {Promise} Promise resolved when done. + */ + requestPasswordReset(siteUrl: string, username?: string, email?: string) : Promise { + var params: any = {}; + + if (username) { + params.username = username; + } + + if (email) { + params.email = email; + } + + return this.wsProvider.callAjax('core_auth_request_password_reset', params, {siteUrl: siteUrl}); + } + + /** + * Check if a confirm should be shown to open a SSO authentication. + * + * @param {number} typeOfLogin CoreConstants.loginSSOCode or CoreConstants.loginSSOInAppCode. + * @return {boolean} True if confirm modal should be shown, false otherwise. + */ + shouldShowSSOConfirm(typeOfLogin: number) : boolean { + return !this.isSSOEmbeddedBrowser(typeOfLogin) && + (!CoreConfigConstants.skipssoconfirmation || String(CoreConfigConstants.skipssoconfirmation) === 'false'); + } + + /** + * Convenient helper to handle get User Token error. It redirects to change password page if forcepassword is set. + * + * @param {string} siteUrl Site URL to construct change password URL. + * @param {any} error Error object containing errorcode and error message. + */ + treatUserTokenError(siteUrl: string, error: any) : void { + if (typeof error == 'string') { + this.domUtils.showErrorModal(error); + } else if (error.errorcode == 'forcepasswordchangenotice') { + this.openChangePassword(siteUrl, error.error); + } else { + this.domUtils.showErrorModal(error.error); + } + } + + /** + * Convenient helper to validate a browser SSO login. + * + * @param {string} url URL received, to be validated. + * @return {Promise} Promise resolved on success. + */ + validateBrowserSSOLogin(url: string) : Promise { + // Split signature:::token + const params = url.split(":::"); + + return this.configProvider.get(CoreConstants.loginLaunchData).then((data): any => { + try { + data = JSON.parse(data); + } catch(ex) { + return Promise.reject(null); + } + + let launchSiteURL = data.siteurl, + passport = data.passport; + + // Reset temporary values. + this.configProvider.delete(CoreConstants.loginLaunchData); + + // Validate the signature. + // We need to check both http and https. + let signature = Md5.hashAsciiStr(launchSiteURL + passport); + if (signature != params[0]) { + if (launchSiteURL.indexOf("https://") != -1) { + launchSiteURL = launchSiteURL.replace("https://", "http://"); + } else { + launchSiteURL = launchSiteURL.replace("http://", "https://"); + } + signature = Md5.hashAsciiStr(launchSiteURL + passport); + } + + if (signature == params[0]) { + this.logger.debug('Signature validated'); + return { + siteUrl: launchSiteURL, + token: params[1], + privateToken: params[2], + pageName: data.pageName, + pageParams: data.pageParams + } + } else { + this.logger.debug('Invalid signature in the URL request yours: ' + params[0] + ' mine: ' + + signature + ' for passport ' + passport); + return Promise.reject(this.translate.instant('mm.core.unexpectederror')); + } + }); + } +} diff --git a/src/providers/app.ts b/src/providers/app.ts index 4ec2c3b39..5b49e96b1 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -21,6 +21,13 @@ import { CoreDbProvider } from './db'; import { CoreLoggerProvider } from './logger'; import { SQLiteDB } from '../classes/sqlitedb'; +export interface CoreRedirectData { + siteId?: string; + page?: string; // Name of the page to redirect. + params?: any; // Params to pass to the page. + timemodified?: number; +}; + /** * Factory to provide some global functionalities, like access to the global app database. * @description @@ -213,16 +220,16 @@ export class CoreAppProvider { /** * Retrieve redirect data. * - * @return {object} Object with siteid, state, params and timemodified. + * @return {CoreRedirectData} Object with siteid, state, params and timemodified. */ - getRedirect() : object { + getRedirect() : CoreRedirectData { if (localStorage && localStorage.getItem) { try { - let data = { - siteid: localStorage.getItem('mmCoreRedirectSiteId'), - state: localStorage.getItem('mmCoreRedirectState'), + let data: CoreRedirectData = { + siteId: localStorage.getItem('mmCoreRedirectSiteId'), + page: localStorage.getItem('mmCoreRedirectState'), params: localStorage.getItem('mmCoreRedirectParams'), - timemodified: localStorage.getItem('mmCoreRedirectTime') + timemodified: parseInt(localStorage.getItem('mmCoreRedirectTime'), 10) }; if (data.params) { @@ -243,9 +250,9 @@ export class CoreAppProvider { * * @param {string} siteId Site ID. * @param {string} page Page to go. - * @param {object} params Page params. + * @param {any} params Page params. */ - storeRedirect(siteId: string, page: string, params: object) : void { + storeRedirect(siteId: string, page: string, params: any) : void { if (localStorage && localStorage.setItem) { try { localStorage.setItem('mmCoreRedirectSiteId', siteId); diff --git a/src/providers/config.ts b/src/providers/config.ts index 0b4fb208c..88cf788c9 100644 --- a/src/providers/config.ts +++ b/src/providers/config.ts @@ -77,10 +77,10 @@ export class CoreConfigProvider { * Set an app setting. * * @param {string} name The config name. - * @param {any} value The config value. Can only store primitive values, not objects. + * @param {boolean|number|string} value The config value. Can only store primitive values, not objects. * @return {Promise} Promise resolved when done. */ - set(name: string, value: any) : Promise { + set(name: string, value: boolean|number|string) : Promise { return this.appDB.insertOrUpdateRecord(this.TABLE_NAME, {name: name, value: value}, {name: name}); } } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 18276a461..21fdcf593 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -86,3 +86,46 @@ $colors: ( @import "roboto"; @import "noto-sans"; + + +// Moodle Mobile variables +// -------------------------------------------------- + +// Color palette +$black: #3a3a3a; // Headings, standard text. +$gray-darker: #626262; // Text (emphasis-detail), placeholder, background. +$gray-dark: #9e9e9e; // Borders (never text). +$gray: #dddddd; +$gray-light: #eeeeee; // Background. +$gray-lighter: #f5f5f5; +$white: #ffffff; // Background, reversed text. + +$blue: #0064d2; // Link, background. +$blue-light: mix($blue, white, 20%); // Background. +$blue-dark: darken($blue, 10%); + +$turquoise: #007982; // Accent. +$turquoise-light: mix($turquoise, white, 20%); // Background. +$turquoise-dark: darken($turquoise, 10%); + +$green: #5e8100; // Accent. +$green-light: mix($green, white, 20%); +$green-dark: darken($green, 10%); + +$red: #cb3d4d; +$red-light: mix($red, white, 20%); +$red-dark: darken($red, 10%); + +$orange: #f98012; // Accent (never text). +$orange-light: lighten($orange, 10%); + +$yellow: #fbad1a; // Accent (never text). +$yellow-light: mix($yellow, white, 20%); +$yellow-dark: mix($yellow, black, 40%); + +// Init screen. +$mm-color-init-screen: #ff5c00; +$mm-color-init-screen-alt: #ff7600; +$mm-init-screen-spinner-color: $white; +$mm-init-screen-logo-width: 60%; +$mm-init-screen-logo-max-width: 300px;