diff --git a/scripts/functions.sh b/scripts/functions.sh index 87e5e5c43..55af8536b 100644 --- a/scripts/functions.sh +++ b/scripts/functions.sh @@ -20,26 +20,22 @@ function print_success { } function print_error { - tput setaf 1; echo " ERROR: $1" - tput setaf 0 + tput setaf 1; echo " ERROR: $1"; tput sgr0 } function print_ok { - tput setaf 2; echo " OK: $1" + tput setaf 2; echo " OK: $1"; tput sgr0 echo - tput setaf 0 } function print_message { - tput setaf 3; echo "-------- $1" + tput setaf 3; echo "-------- $1"; tput sgr0 echo - tput setaf 0 } function print_title { stepnumber=$(($stepnumber + 1)) echo - tput setaf 5; echo "$stepnumber $1" - tput setaf 5; echo '==================' - tput setaf 0 + tput setaf 5; echo "$stepnumber $1"; tput sgr0 + tput setaf 5; echo '=================='; tput sgr0 } \ No newline at end of file diff --git a/scripts/langindex.json b/scripts/langindex.json index 6e12a847e..87413dde6 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1567,7 +1567,7 @@ "core.login.changepassword": "moodle", "core.login.changepasswordbutton": "local_moodlemobileapp", "core.login.changepasswordhelp": "local_moodlemobileapp", - "core.login.changepassowrdinstructions": "local_moodlemobileapp", + "core.login.changepasswordinstructions": "local_moodlemobileapp", "core.login.changepasswordlogoutinstructions": "local_moodlemobileapp", "core.login.changepasswordreconnectinstructions": "local_moodlemobileapp", "core.login.checksiteversion": "local_moodlemobileapp", @@ -1925,6 +1925,8 @@ "core.unknown": "local_moodlemobileapp", "core.unlimited": "moodle", "core.unzipping": "local_moodlemobileapp", + "core.updaterequired": "local_moodlemobileapp", + "core.updaterequireddesc": "local_moodlemobileapp", "core.upgraderunning": "error", "core.user": "moodle", "core.user.address": "moodle", diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 20432a540..2b50cbff9 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1562,10 +1562,10 @@ "core.login.auth_email": "Email-based self-registration", "core.login.authenticating": "Authenticating", "core.login.cancel": "Cancel", - "core.login.changepassowrdinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.", "core.login.changepassword": "Change password", "core.login.changepasswordbutton": "Open the change password page", "core.login.changepasswordhelp": "If you have problems changing your password, please contact your site administrator. \"Site Administrators\" are the people who manages the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.", + "core.login.changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.", "core.login.changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:", "core.login.changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).", "core.login.checksiteversion": "Check that your site uses Moodle 3.1 or later.", @@ -1920,6 +1920,8 @@ "core.unknown": "Unknown", "core.unlimited": "Unlimited", "core.unzipping": "Unzipping", + "core.updaterequired": "App update required", + "core.updaterequireddesc": "Please update your app to version {{$a}}", "core.upgraderunning": "Site is being upgraded, please retry later.", "core.user": "User", "core.user.address": "Address", diff --git a/src/components/loading/loading.scss b/src/components/loading/loading.scss index d4c5916ba..6b5bbaa21 100644 --- a/src/components/loading/loading.scss +++ b/src/components/loading/loading.scss @@ -25,8 +25,8 @@ ion-app.app-root { padding-left: 0 !important; padding-right: 0 !important; - > .core-loading-content > *, - > .core-loading-content-loading > * { + > .core-loading-content > *:not[padding], + > .core-loading-content-loading > *:not[padding] { @include safe-area-padding-horizontal(0px, 0px); } } diff --git a/src/core/login/lang/en.json b/src/core/login/lang/en.json index a3c443bc9..e4780131c 100644 --- a/src/core/login/lang/en.json +++ b/src/core/login/lang/en.json @@ -6,7 +6,7 @@ "changepassword": "Change password", "changepasswordbutton": "Open the change password page", "changepasswordhelp": "If you have problems changing your password, please contact your site administrator. \"Site Administrators\" are the people who manages the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.", - "changepassowrdinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.", + "changepasswordinstructions": "You cannot change your password in the app. Please click the following button to open the site in a web browser to change your password. Take into account you need to close the browser after changing the password as you will not be redirected to the app.", "changepasswordlogoutinstructions": "If you prefer to change site or log out, please click the following button:", "changepasswordreconnectinstructions": "Click the following button to reconnect to the site. (Take into account that if you didn't change your password successfully, you would return to the previous screen).", "confirmdeletesite": "Are you sure you want to delete the site {{sitename}}?", diff --git a/src/core/login/login.module.ts b/src/core/login/login.module.ts index 524654955..05be940c0 100644 --- a/src/core/login/login.module.ts +++ b/src/core/login/login.module.ts @@ -13,12 +13,15 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreLoginHelperProvider } from './providers/helper'; +import { CoreLoginCronHandler } from './providers/cron-handler'; import { CoreLoginSitesPageModule } from './pages/sites/sites.module'; // List of providers. export const CORE_LOGIN_PROVIDERS = [ - CoreLoginHelperProvider + CoreLoginHelperProvider, + CoreLoginCronHandler ]; @NgModule({ @@ -29,4 +32,9 @@ export const CORE_LOGIN_PROVIDERS = [ ], providers: CORE_LOGIN_PROVIDERS }) -export class CoreLoginModule {} +export class CoreLoginModule { + constructor(cronDelegate: CoreCronDelegate, cronHandler: CoreLoginCronHandler) { + // Register handlers. + cronDelegate.register(cronHandler); + } +} diff --git a/src/core/login/pages/change-password/change-password.html b/src/core/login/pages/change-password/change-password.html index 9bbd96064..7c965a3a0 100644 --- a/src/core/login/pages/change-password/change-password.html +++ b/src/core/login/pages/change-password/change-password.html @@ -12,7 +12,7 @@

{{ 'core.login.forcepasswordchangenotice' | translate }}

-

{{ 'core.login.changepassowrdinstructions' | translate }}

+

{{ 'core.login.changepasswordinstructions' | translate }}

diff --git a/src/core/login/pages/init/init.ts b/src/core/login/pages/init/init.ts index 0ca100be9..b7ea64352 100644 --- a/src/core/login/pages/init/init.ts +++ b/src/core/login/pages/init/init.ts @@ -85,15 +85,22 @@ export class CoreLoginInitPage { */ protected loadPage(): Promise { if (this.sitesProvider.isLoggedIn()) { - if (!this.loginHelper.isSiteLoggedOut()) { - // User is logged in, go to site initial page. - return this.loginHelper.goToSiteInitialPage(); - } else { - // The site is marked as logged out. Logout and try again. + if (this.loginHelper.isSiteLoggedOut()) { return this.sitesProvider.logout().then(() => { return this.loadPage(); }); } + + return this.sitesProvider.getCurrentSite().getPublicConfig().catch(() => { + return {}; + }).then((config) => { + return this.sitesProvider.checkRequiredMinimumVersion(config).then(() => { + // User is logged in, go to site initial page. + return this.loginHelper.goToSiteInitialPage(); + }).catch(() => { + return this.loadPage(); + }); + }); } return this.navCtrl.setRoot('CoreLoginSitesPage'); diff --git a/src/core/login/pages/reconnect/reconnect.ts b/src/core/login/pages/reconnect/reconnect.ts index 33ad5b0db..3ec8479a3 100644 --- a/src/core/login/pages/reconnect/reconnect.ts +++ b/src/core/login/pages/reconnect/reconnect.ts @@ -82,16 +82,20 @@ export class CoreLoginReconnectPage { this.siteUrl = site.infos.siteurl; this.siteName = site.getSiteName(); - // Check logoURL if user avatar is not set. - if (this.site.avatar.startsWith(site.infos.siteurl + '/theme/image.php')) { - this.site.avatar = false; + return site.getPublicConfig().then((config) => { + return this.sitesProvider.checkRequiredMinimumVersion(config).then(() => { + // Check logoURL if user avatar is not set. + if (this.site.avatar.startsWith(site.infos.siteurl + '/theme/image.php')) { + this.site.avatar = false; - return site.getPublicConfig().then((config) => { - this.logoUrl = config.logourl || config.compactlogourl; + this.logoUrl = config.logourl || config.compactlogourl; + } }).catch(() => { - // Ignore errors. + this.cancel(); }); - } + }).catch(() => { + // Ignore errors. + }); }).catch(() => { // Shouldn't happen. Just leave the view. this.cancel(); diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index f96eb9c3e..f9bc3a139 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -112,18 +112,21 @@ export class CoreLoginSitePage { } else { // Not a demo site. this.sitesProvider.checkSite(url).then((result) => { + return this.sitesProvider.checkRequiredMinimumVersion(result.config).then(() => { + if (result.warning) { + this.domUtils.showErrorModal(result.warning, true, 4000); + } - if (result.warning) { - this.domUtils.showErrorModal(result.warning, true, 4000); - } - - if (this.loginHelper.isSSOLoginNeeded(result.code)) { - // SSO. User needs to authenticate in a browser. - this.loginHelper.confirmAndOpenBrowserForSSOLogin( - result.siteUrl, result.code, result.service, result.config && result.config.launchurl); - } else { - this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: result.siteUrl, siteConfig: result.config }); - } + if (this.loginHelper.isSSOLoginNeeded(result.code)) { + // SSO. User needs to authenticate in a browser. + this.loginHelper.confirmAndOpenBrowserForSSOLogin( + result.siteUrl, result.code, result.service, result.config && result.config.launchurl); + } else { + this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: result.siteUrl, siteConfig: result.config }); + } + }).catch(() => { + // Ignore errors. + }); }, (error) => { this.showLoginIssue(url, error); }).finally(() => { diff --git a/src/core/login/providers/cron-handler.ts b/src/core/login/providers/cron-handler.ts new file mode 100644 index 000000000..f821b7172 --- /dev/null +++ b/src/core/login/providers/cron-handler.ts @@ -0,0 +1,64 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { CoreSitesProvider } from '@providers/sites'; + +/** + * Cron handler to log out sites when does not meet the app requirements. + */ +@Injectable() +export class CoreLoginCronHandler implements CoreCronHandler { + name = 'CoreLoginCronHandler'; + + constructor(private sitesProvider: CoreSitesProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @return Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (!siteId) { + return Promise.resolve(); + } + + // Check logged in site minimun required version. + // Do not check twice in the same 10 minutes. + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getPublicConfig().catch(() => { + return {}; + }).then((config) => { + this.sitesProvider.checkRequiredMinimumVersion(config).catch(() => { + // Ignore errors. + + }); + }); + }); + } + + /** + * Check whether it's a synchronization process or not. True if not defined. + * + * @return Whether it's a synchronization process or not. + */ + isSync(): boolean { + // Defined to true to be checked on sync site. + return true; + } +} diff --git a/src/core/settings/providers/helper.ts b/src/core/settings/providers/helper.ts index dbbeb4510..b5b48ab05 100644 --- a/src/core/settings/providers/helper.ts +++ b/src/core/settings/providers/helper.ts @@ -171,9 +171,9 @@ export class CoreSettingsHelper { }); })); - const syncPromise = Promise.all(promises); + let syncPromise = Promise.all(promises); this.syncPromises[siteId] = syncPromise; - syncPromise.finally(() => { + syncPromise = syncPromise.finally(() => { delete this.syncPromises[siteId]; }); diff --git a/src/lang/en.json b/src/lang/en.json index 7b7fc192f..2d0e8125c 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -273,6 +273,8 @@ "unlimited": "Unlimited", "unzipping": "Unzipping", "upgraderunning": "Site is being upgraded, please retry later.", + "updaterequired": "App update required", + "updaterequireddesc": "Please update your app to version {{$a}}", "user": "User", "userdeleted": "This user account has been deleted", "userdetails": "User details", diff --git a/src/providers/app.ts b/src/providers/app.ts index 615b68ecb..4129fe48e 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -170,6 +170,15 @@ export class CoreAppProvider { return this.isDesktop() && process.arch == 'x64'; } + /** + * Checks if the app is running in an Android mobile or tablet device. + * + * @return Whether the app is running in an Android mobile or tablet device. + */ + isAndroid(): boolean { + return this.platform.is('android'); + } + /** * Checks if the app is running in a desktop environment (not browser). * @@ -181,6 +190,15 @@ export class CoreAppProvider { return !!(process && process.versions && typeof process.versions.electron != 'undefined'); } + /** + * Checks if the app is running in an iOS mobile or tablet device. + * + * @return Whether the app is running in an iOS mobile or tablet device. + */ + isIOS(): boolean { + return this.platform.is('ios'); + } + /** * Check if the keyboard is visible. * diff --git a/src/providers/sites.ts b/src/providers/sites.ts index dd3b66992..ffde5b3d6 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -19,6 +19,7 @@ import { CoreAppProvider } from './app'; import { CoreEventsProvider } from './events'; import { CoreLoggerProvider } from './logger'; import { CoreSitesFactoryProvider } from './sites-factory'; +import { CoreDomUtilsProvider } from './utils/dom'; import { CoreTextUtilsProvider } from './utils/text'; import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; @@ -311,7 +312,8 @@ export class CoreSitesProvider { constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, - private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider) { + private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider, + protected domUtils: CoreDomUtilsProvider) { this.logger = logger.getInstance('CoreSitesProvider'); this.appDB = appProvider.getDB(); @@ -862,6 +864,87 @@ export class CoreSitesProvider { return this.appDB.insertRecord(this.SITES_TABLE, entry); } + /** + * Check the required minimum version of the app for a site and shows a download dialog. + * + * @param config Config object of the site. + * @param siteId ID of the site to check. Current site id will be used otherwise. + * @return Resolved with if meets the requirements, rejected otherwise. + */ + checkRequiredMinimumVersion(config: any, siteId?: string): Promise { + if (config && config.tool_mobile_minimumversion) { + const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion), + appVersion = this.convertVersionName(CoreConfigConstants.versionname); + + if (requiredVersion > appVersion) { + let downloadUrl = ''; + + if (this.appProvider.isAndroid() && config.tool_mobile_androidappid) { + downloadUrl = 'market://details?id=' + config.tool_mobile_androidappid; + } else if (this.appProvider.isIOS() && config.tool_mobile_iosappid) { + downloadUrl = 'itms-apps://itunes.apple.com/app/id' + config.tool_mobile_iosappid; + } else if (config.tool_mobile_setuplink) { + downloadUrl = config.tool_mobile_setuplink; + } else if (this.appProvider.isMobile()) { + downloadUrl = 'https://download.moodle.org/mobile/'; + } else { + downloadUrl = 'https://download.moodle.org/desktop/'; + } + + siteId = siteId || this.getCurrentSiteId(); + + // Do not block interface. + this.domUtils.showConfirm( + this.translate.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }), + this.translate.instant('core.updaterequired'), + this.translate.instant('core.download'), + this.translate.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel')).then(() => { + + this.utils.openInBrowser(downloadUrl); + }).catch(() => { + // Do nothing. + }); + + if (siteId) { + // Logout if it's the currentSite. + const promise = siteId == this.getCurrentSiteId() ? this.logout() : Promise.resolve(); + + return promise.then(() => { + // Always expire the token. + return this.setSiteLoggedOut(siteId, true); + }).then(() => { + return Promise.reject(null); + }); + } + + return Promise.reject(null); + } + } + + return Promise.resolve(); + } + + /** + * Convert version name to numbers. + * + * @param name Version name (dot separated). + * @return Version translated to a comparable number. + */ + protected convertVersionName(name: string): number { + let version = 0; + + const parts = name.split('.', 3); + parts.forEach((num) => { + version = (version * 100) + Number(num); + }); + + if (parts.length < 3) { + version = version * Math.pow(100, 3 - parts.length); + } + + return version; + } + /** * Login a user to a site from the list of sites. * @@ -896,12 +979,20 @@ export class CoreSitesProvider { return false; }, () => { - this.login(siteId); + return site.getPublicConfig().catch(() => { + return {}; + }).then((config) => { + return this.checkRequiredMinimumVersion(config).then(() => { + this.login(siteId); - // Update site info. We don't block the UI. - this.updateSiteInfo(siteId); + // Update site info. We don't block the UI. + this.updateSiteInfo(siteId); - return true; + return true; + }).catch(() => { + return false; + }); + }); }); }); }