diff --git a/config.xml b/config.xml index 0571f0bd8..dce611f4a 100644 --- a/config.xml +++ b/config.xml @@ -269,6 +269,9 @@ + + This app needs third party cookies to correctly render embedded content from the Moodle site. + diff --git a/scripts/langindex.json b/scripts/langindex.json index db5d12893..e96142599 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1478,6 +1478,7 @@ "core.coursenogroups": "local_moodlemobileapp", "core.courses.addtofavourites": "block_myoverview", "core.courses.allowguests": "enrol_guest", + "core.courses.aria:coursecategory": "course", "core.courses.aria:coursename": "course", "core.courses.aria:courseprogress": "block_myoverview", "core.courses.aria:favourite": "course", @@ -1740,10 +1741,12 @@ "core.hour": "moodle", "core.hours": "moodle", "core.humanreadablesize": "local_moodlemobileapp", + "core.iframehelp": "local_moodlemobileapp", "core.image": "local_moodlemobileapp", "core.imageviewer": "local_moodlemobileapp", "core.info": "moodle", "core.invalidformdata": "error", + "core.ioscookieshelp": "local_moodlemobileapp", "core.labelsep": "langconfig", "core.lastaccess": "moodle", "core.lastdownloaded": "local_moodlemobileapp", @@ -1950,6 +1953,7 @@ "core.openfullimage": "local_moodlemobileapp", "core.openinbrowser": "local_moodlemobileapp", "core.openmodinbrowser": "local_moodlemobileapp", + "core.opensettings": "local_moodlemobileapp", "core.othergroups": "group", "core.pagea": "moodle", "core.parentlanguage": "langconfig", @@ -2059,6 +2063,8 @@ "core.settings.fontsizecharacter": "block_accessibility/char", "core.settings.forcedsetting": "local_moodlemobileapp", "core.settings.general": "moodle", + "core.settings.ioscookies": "local_moodlemobileapp", + "core.settings.ioscookiesdescription": "local_moodlemobileapp", "core.settings.language": "moodle", "core.settings.license": "moodle", "core.settings.localnotifavailable": "local_moodlemobileapp", diff --git a/src/addons/mod/scorm/scorm.module.ts b/src/addons/mod/scorm/scorm.module.ts index 45d28649e..f4dc142e6 100644 --- a/src/addons/mod/scorm/scorm.module.ts +++ b/src/addons/mod/scorm/scorm.module.ts @@ -19,6 +19,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreCronDelegate } from '@services/cron'; +import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { AddonModScormComponentsModule } from './components/components.module'; import { OFFLINE_SITE_SCHEMA } from './services/database/scorm'; @@ -26,6 +27,7 @@ import { AddonModScormGradeLinkHandler } from './services/handlers/grade-link'; import { AddonModScormIndexLinkHandler } from './services/handlers/index-link'; import { AddonModScormListLinkHandler } from './services/handlers/list-link'; import { AddonModScormModuleHandler, AddonModScormModuleHandlerService } from './services/handlers/module'; +import { AddonModScormPluginFileHandler } from './services/handlers/pluginfile'; import { AddonModScormPrefetchHandler } from './services/handlers/prefetch'; import { AddonModScormSyncCronHandler } from './services/handlers/sync-cron'; import { AddonModScormProvider } from './services/scorm'; @@ -69,6 +71,7 @@ const routes: Routes = [ CoreContentLinksDelegate.registerHandler(AddonModScormGradeLinkHandler.instance); CoreContentLinksDelegate.registerHandler(AddonModScormIndexLinkHandler.instance); CoreContentLinksDelegate.registerHandler(AddonModScormListLinkHandler.instance); + CorePluginFileDelegate.registerHandler(AddonModScormPluginFileHandler.instance); }, }, ], diff --git a/src/addons/notes/services/notes-sync.ts b/src/addons/notes/services/notes-sync.ts index 29dbefe92..2df96b2ce 100644 --- a/src/addons/notes/services/notes-sync.ts +++ b/src/addons/notes/services/notes-sync.ts @@ -45,17 +45,17 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { - return this.syncOnSites('all notes', this.syncAllNotesFunc.bind(this, siteId, force), siteId); + return this.syncOnSites('all notes', this.syncAllNotesFunc.bind(this, !!force), siteId); } /** * Synchronize all the notes in a certain site * - * @param siteId Site ID to sync. * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. * @return Promise resolved if sync is successful, rejected if sync fails. */ - protected async syncAllNotesFunc(siteId: string, force: boolean): Promise { + protected async syncAllNotesFunc(force: boolean, siteId: string): Promise { const notesArray = await Promise.all([ AddonNotesOffline.getAllNotes(siteId), AddonNotesOffline.getAllDeletedNotes(siteId), diff --git a/src/core/components/iframe/core-iframe.html b/src/core/components/iframe/core-iframe.html index 1c05ee6e4..def2619f3 100644 --- a/src/core/components/iframe/core-iframe.html +++ b/src/core/components/iframe/core-iframe.html @@ -5,7 +5,13 @@ [attr.allowfullscreen]="allowFullscreen ? 'allowfullscreen' : null"> + + + {{ 'core.iframehelp' | translate }} + + + - \ No newline at end of file + diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index bc4c7a656..cb1ecd059 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -40,6 +40,7 @@ export class CoreIframeComponent implements OnChanges { loading?: boolean; safeUrl?: SafeResourceUrl; + displayHelp = false; protected readonly IFRAME_TIMEOUT = 15000; protected logger: CoreLogger; @@ -100,6 +101,7 @@ export class CoreIframeComponent implements OnChanges { async ngOnChanges(changes: {[name: string]: SimpleChange }): Promise { if (changes.src) { const url = CoreUrlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue; + this.displayHelp = CoreIframeUtils.shouldDisplayHelpForUrl(url); await CoreIframeUtils.fixIframeCookies(url); @@ -112,4 +114,11 @@ export class CoreIframeComponent implements OnChanges { } } + /** + * Open help modal for iframes. + */ + openIframeHelpModal(): void { + CoreIframeUtils.openIframeHelpModal(); + } + } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 4ceb07fea..0b1e0c388 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -682,6 +682,10 @@ export class CoreFormatTextDirective implements OnChanges { this.addMediaAdaptClass(iframe); + if (CoreIframeUtils.shouldDisplayHelpForUrl(src)) { + this.addIframeHelp(iframe); + } + if (currentSite?.containsUrl(src)) { // URL points to current site, try to use auto-login. const finalUrl = await currentSite.getAutoLoginUrl(src, false); @@ -757,6 +761,32 @@ export class CoreFormatTextDirective implements OnChanges { CoreIframeUtils.treatFrame(iframe, false); } + /** + * Add iframe help option. + * + * @param iframe Iframe. + */ + protected addIframeHelp(iframe: HTMLIFrameElement): void { + const helpDiv = document.createElement('div'); + + helpDiv.classList.add('ion-text-center'); + helpDiv.classList.add('ion-text-wrap'); + helpDiv.classList.add('core-iframe-help'); + + const button = document.createElement('ion-button'); + button.setAttribute('fill', 'clear'); + button.setAttribute('aria-haspopup', 'dialog'); + button.innerHTML = Translate.instant('core.iframehelp'); + + button.addEventListener('click', () => { + CoreIframeUtils.openIframeHelpModal(); + }); + + helpDiv.appendChild(button); + + iframe.after(helpDiv); + } + /** * Convert window.open to window.openWindowSafely inside HTML tags. * diff --git a/src/core/features/block/services/block-delegate.ts b/src/core/features/block/services/block-delegate.ts index eeb0e8832..67e80bb65 100644 --- a/src/core/features/block/services/block-delegate.ts +++ b/src/core/features/block/services/block-delegate.ts @@ -20,6 +20,7 @@ import { Subject } from 'rxjs'; import { CoreCourseBlock } from '@features/course/services/course'; import { Params } from '@angular/router'; import { makeSingleton } from '@singletons'; +import { CoreBlockDefaultHandler } from './handlers/default-block'; /** * Interface that all blocks must implement. @@ -93,7 +94,9 @@ export class CoreBlockDelegateService extends CoreDelegate { blocksUpdateObservable: Subject; - constructor() { + constructor( + protected defaultHandler: CoreBlockDefaultHandler, + ) { super('CoreBlockDelegate', true); this.blocksUpdateObservable = new Subject(); diff --git a/src/core/features/comments/services/comments-sync.ts b/src/core/features/comments/services/comments-sync.ts index 1c91862d6..1055c8c4b 100644 --- a/src/core/features/comments/services/comments-sync.ts +++ b/src/core/features/comments/services/comments-sync.ts @@ -44,17 +44,17 @@ export class CoreCommentsSyncProvider extends CoreSyncBaseProvider { - return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this, siteId, force), siteId); + return this.syncOnSites('all comments', this.syncAllCommentsFunc.bind(this, !!force), siteId); } /** * Synchronize all the comments in a certain site * - * @param siteId Site ID to sync. * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. * @return Promise resolved if sync is successful, rejected if sync fails. */ - private async syncAllCommentsFunc(siteId: string, force: boolean): Promise { + private async syncAllCommentsFunc(force: boolean, siteId: string): Promise { const comments = await CoreCommentsOffline.getAllComments(siteId); const commentsUnique: { [syncId: string]: (CoreCommentsDBRecord | CoreCommentsDeletedDBRecord) } = {}; diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index 75a8e77c7..235541150 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule } from '@angular/core'; import { Routes } from '@angular/router'; import { AppRoutingModule } from '@/app/app-routing.module'; import { CoreLoginHelperProvider } from './services/login-helper'; import { CoreRedirectGuard } from '@guards/redirect'; +import { CoreLoginCronHandler } from './services/handlers/cron'; +import { CoreCronDelegate } from '@services/cron'; export const CORE_LOGIN_SERVICES = [ CoreLoginHelperProvider, @@ -34,5 +36,15 @@ const appRoutes: Routes = [ @NgModule({ imports: [AppRoutingModule.forChild(appRoutes)], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => async () => { + CoreCronDelegate.register(CoreLoginCronHandler.instance); + }, + }, + ], }) export class CoreLoginModule {} diff --git a/src/core/features/login/services/handlers/cron.ts b/src/core/features/login/services/handlers/cron.ts new file mode 100644 index 000000000..826306c60 --- /dev/null +++ b/src/core/features/login/services/handlers/cron.ts @@ -0,0 +1,58 @@ +// (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 { CoreSitePublicConfigResponse } from '@classes/site'; +import { CoreCronHandler } from '@services/cron'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; + +/** + * Cron handler to log out sites when does not meet the app requirements. + */ +@Injectable({ providedIn: 'root' }) +export class CoreLoginCronHandlerService implements CoreCronHandler { + + name = 'CoreLoginCronHandler'; + + /** + * @inheritdoc + */ + async execute(siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + if (!siteId) { + return; + } + + // Check logged in site minimun required version. + // Do not check twice in the same 10 minutes. + const site = await CoreSites.getSite(siteId); + + const config = await CoreUtils.ignoreErrors(site.getPublicConfig(), > {}); + + CoreUtils.ignoreErrors(CoreSites.checkApplication( config)); + } + + /** + * @inheritdoc + */ + isSync(): boolean { + // Defined to true to be checked on sync site. + return true; + } + +} + +export const CoreLoginCronHandler = makeSingleton(CoreLoginCronHandlerService); diff --git a/src/core/features/settings/lang.json b/src/core/features/settings/lang.json index 9252a449f..b1580a355 100644 --- a/src/core/features/settings/lang.json +++ b/src/core/features/settings/lang.json @@ -42,6 +42,8 @@ "fontsizecharacter": "A", "forcedsetting": "This setting has been forced by your site configuration.", "general": "General", + "ioscookies": "Cross-Website Tracking", + "ioscookiesdescription": "Embedded content from the site might require cross-site cookies to work. To enable it, please go to the app's iOS settings and enable 'Allow Cross-Website Tracking'.", "language": "Language", "license": "Licence", "localnotifavailable": "Local notifications available", diff --git a/src/core/features/settings/pages/general/general.html b/src/core/features/settings/pages/general/general.html index bb88ee283..c16a1e80c 100644 --- a/src/core/features/settings/pages/general/general.html +++ b/src/core/features/settings/pages/general/general.html @@ -54,6 +54,15 @@ + + + {{ 'core.settings.ioscookies' | translate }} + {{ 'core.settings.ioscookiesdescription' | translate }} + + {{ 'core.opensettings' | translate }} + + + {{ 'core.settings.debugdisplay' | translate }} diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index ff2270908..344164a37 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -21,6 +21,8 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreSettingsHelper, CoreColorScheme, CoreZoomLevel } from '../../services/settings-helper'; import { CoreApp } from '@services/app'; +import { CoreIframeUtils } from '@services/utils/iframe'; +import { Diagnostic } from '@singletons'; /** * Page that displays the general settings. @@ -44,6 +46,7 @@ export class CoreSettingsGeneralPage { selectedScheme: CoreColorScheme = CoreColorScheme.LIGHT; colorSchemeDisabled = false; isAndroid = false; + displayIframeHelp = false; constructor() { this.asyncInit(); @@ -98,6 +101,8 @@ export class CoreSettingsGeneralPage { if (this.analyticsSupported) { this.analyticsEnabled = await CoreConfig.get(CoreConstants.SETTINGS_ANALYTICS_ENABLED, true); } + + this.displayIframeHelp = CoreIframeUtils.shouldDisplayHelp(); } /** @@ -155,4 +160,11 @@ export class CoreSettingsGeneralPage { CoreConfig.set(CoreConstants.SETTINGS_ANALYTICS_ENABLED, this.analyticsEnabled ? 1 : 0); } + /** + * Open native settings. + */ + openNativeSettings(): void { + Diagnostic.switchToSettings(); + } + } diff --git a/src/core/lang.json b/src/core/lang.json index a2b8c14b5..bc1cf8e64 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -129,10 +129,12 @@ "hour": "hour", "hours": "hours", "humanreadablesize": "{{size}} {{unit}}", + "iframehelp": "Is this content not working?", "image": "Image", "imageviewer": "Image viewer", "info": "Information", "invalidformdata": "Incorrect form data", + "ioscookieshelp": "Embedded content might require cookies to work. Please go to the app's iOS settings, enable 'Allow Cross-Website Tracking' and try again.", "labelsep": ":", "filter": "Filter", "lastaccess": "Last access", @@ -213,6 +215,7 @@ "openfullimage": "Click here to display the full size image", "openinbrowser": "Open in browser", "openmodinbrowser": "Open {{$a}} in browser", + "opensettings": "Open settings", "othergroups": "Other groups", "pagea": "Page {{$a}}", "parentlanguage": "", diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 064f24380..35e17cfbb 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -25,7 +25,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { makeSingleton, Network, Platform, NgZone, Translate } from '@singletons'; +import { makeSingleton, Network, Platform, NgZone, Translate, Diagnostic } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; @@ -558,6 +558,47 @@ export class CoreIframeUtilsProvider { } } + /** + * Check whether the help should be displayed in current OS. + * + * @return Boolean. + */ + shouldDisplayHelp(): boolean { + return CoreApp.isIOS() && CoreApp.getPlatformMajorVersion() >= 14; + } + + /** + * Check whether the help should be displayed for a certain iframe. + * + * @param url Iframe URL. + * @return Boolean. + */ + shouldDisplayHelpForUrl(url: string): boolean { + return this.shouldDisplayHelp() && !CoreUrlUtils.isLocalFileUrl(url); + } + + /** + * Open help modal for iframes. + */ + openIframeHelpModal(): void { + CoreDomUtils.showAlertWithOptions({ + header: Translate.instant('core.settings.ioscookies'), + message: Translate.instant('core.ioscookieshelp'), + buttons: [ + { + text: Translate.instant('core.cancel'), + role: 'cancel', + }, + { + text: Translate.instant('core.opensettings'), + handler: (): void => { + Diagnostic.switchToSettings(); + }, + }, + ], + }); + } + } export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider); diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 65735f44e..1e33ab0ef 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -526,3 +526,9 @@ img.core-media-adapt-width { audio.core-media-adapt-width { width: 100%; } + +.core-iframe-help ion-button { + text-transform: none; + text-decoration: underline; + --color: initial; +}
{{ 'core.settings.ioscookiesdescription' | translate }}