diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts
index b16efbbcb..a6bf120a8 100644
--- a/src/app/components/components.module.ts
+++ b/src/app/components/components.module.ts
@@ -18,6 +18,7 @@ import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreIconComponent } from './icon/icon';
+import { CoreIframeComponent } from './iframe/iframe';
import { CoreLoadingComponent } from './loading/loading';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
@@ -27,6 +28,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
@NgModule({
declarations: [
CoreIconComponent,
+ CoreIframeComponent,
CoreLoadingComponent,
CoreShowPasswordComponent,
CoreEmptyBoxComponent,
@@ -40,6 +42,7 @@ import { CorePipesModule } from '@app/pipes/pipes.module';
],
exports: [
CoreIconComponent,
+ CoreIframeComponent,
CoreLoadingComponent,
CoreShowPasswordComponent,
CoreEmptyBoxComponent,
diff --git a/src/app/components/iframe/core-iframe.html b/src/app/components/iframe/core-iframe.html
new file mode 100644
index 000000000..1c05ee6e4
--- /dev/null
+++ b/src/app/components/iframe/core-iframe.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/components/iframe/iframe.scss b/src/app/components/iframe/iframe.scss
new file mode 100644
index 000000000..d99e54aae
--- /dev/null
+++ b/src/app/components/iframe/iframe.scss
@@ -0,0 +1,31 @@
+ion-app.app-root core-iframe {
+
+ > div {
+ max-width: 100%;
+ max-height: 100%;
+ }
+ iframe {
+ border: 0;
+ display: block;
+ max-width: 100%;
+ background-color: $gray-light;
+ }
+
+ .core-loading-container {
+ position: absolute;
+ @include position(0, 0, 0, 0);
+ display: table;
+ height: 100%;
+ width: 100%;
+ z-index: 1;
+ margin: 0;
+ padding: 0;
+ clear: both;
+
+ .core-loading-spinner {
+ display: table-cell;
+ text-align: center;
+ vertical-align: middle;
+ }
+ }
+}
diff --git a/src/app/components/iframe/iframe.ts b/src/app/components/iframe/iframe.ts
new file mode 100644
index 000000000..c3fa0e8b2
--- /dev/null
+++ b/src/app/components/iframe/iframe.ts
@@ -0,0 +1,117 @@
+// (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, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange,
+} from '@angular/core';
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+import { NavController } from '@ionic/angular';
+
+import { CoreFile } from '@services/file';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreUrlUtils } from '@services/utils/url';
+import { CoreIframeUtils } from '@services/utils/iframe';
+import { CoreUtils } from '@services/utils/utils';
+import { CoreLogger } from '@singletons/logger';
+
+@Component({
+ selector: 'core-iframe',
+ templateUrl: 'core-iframe.html',
+})
+export class CoreIframeComponent implements OnChanges {
+
+ @ViewChild('iframe') iframe?: ElementRef;
+ @Input() src?: string;
+ @Input() iframeWidth?: string;
+ @Input() iframeHeight?: string;
+ @Input() allowFullscreen?: boolean | string;
+ @Output() loaded: EventEmitter = new EventEmitter();
+
+ loading?: boolean;
+ safeUrl?: SafeResourceUrl;
+
+ protected readonly IFRAME_TIMEOUT = 15000;
+ protected logger: CoreLogger;
+ protected initialized = false;
+
+ constructor(
+ protected sanitizer: DomSanitizer,
+ protected navCtrl: NavController,
+ ) {
+
+ this.logger = CoreLogger.getInstance('CoreIframe');
+ this.loaded = new EventEmitter();
+ }
+
+ /**
+ * Init the data.
+ */
+ protected init(): void {
+ if (this.initialized) {
+ return;
+ }
+
+ const iframe: HTMLIFrameElement | undefined = this.iframe?.nativeElement;
+ if (!iframe) {
+ return;
+ }
+
+ this.initialized = true;
+
+ this.iframeWidth = (this.iframeWidth && CoreDomUtils.instance.formatPixelsSize(this.iframeWidth)) || '100%';
+ this.iframeHeight = (this.iframeHeight && CoreDomUtils.instance.formatPixelsSize(this.iframeHeight)) || '100%';
+ this.allowFullscreen = CoreUtils.instance.isTrueOrOne(this.allowFullscreen);
+
+ // Show loading only with external URLs.
+ this.loading = !this.src || !CoreUrlUtils.instance.isLocalFileUrl(this.src);
+
+ // @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
+ CoreIframeUtils.instance.treatFrame(iframe, false, this.navCtrl);
+
+ iframe.addEventListener('load', () => {
+ this.loading = false;
+ this.loaded.emit(iframe); // Notify iframe was loaded.
+ });
+
+ iframe.addEventListener('error', () => {
+ this.loading = false;
+ CoreDomUtils.instance.showErrorModal('core.errorloadingcontent', true);
+ });
+
+ if (this.loading) {
+ setTimeout(() => {
+ this.loading = false;
+ }, this.IFRAME_TIMEOUT);
+ }
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ async ngOnChanges(changes: {[name: string]: SimpleChange }): Promise {
+ if (changes.src) {
+ const url = CoreUrlUtils.instance.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue;
+
+ await CoreIframeUtils.instance.fixIframeCookies(url);
+
+ this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url));
+
+ // Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM.
+ setTimeout(() => {
+ this.init();
+ });
+ }
+ }
+
+}
diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts
index 0263baf61..dd76dd2b0 100644
--- a/src/app/core/login/login-routing.module.ts
+++ b/src/app/core/login/login-routing.module.ts
@@ -47,6 +47,10 @@ const routes: Routes = [
loadChildren: () => import('./pages/change-password/change-password.module')
.then( m => m.CoreLoginChangePasswordPageModule),
},
+ {
+ path: 'sitepolicy',
+ loadChildren: () => import('./pages/site-policy/site-policy.module').then( m => m.CoreLoginSitePolicyPageModule),
+ },
];
@NgModule({
diff --git a/src/app/core/login/pages/site-policy/site-policy.html b/src/app/core/login/pages/site-policy/site-policy.html
new file mode 100644
index 000000000..81e3537fc
--- /dev/null
+++ b/src/app/core/login/pages/site-policy/site-policy.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ {{ 'core.login.policyagreement' | translate }}
+
+
+
+
+
+
+ {{ 'core.login.policyagree' | translate }}
+
+
+
+ {{ 'core.login.policyagreementclick' | translate }}
+
+
+
+
+
+
+ {{ 'core.login.policyaccept' | translate }}
+
+
+ {{ 'core.login.cancel' | translate }}
+
+
+
+
diff --git a/src/app/core/login/pages/site-policy/site-policy.module.ts b/src/app/core/login/pages/site-policy/site-policy.module.ts
new file mode 100644
index 000000000..3635639c2
--- /dev/null
+++ b/src/app/core/login/pages/site-policy/site-policy.module.ts
@@ -0,0 +1,46 @@
+// (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 { 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 { CoreLoginSitePolicyPage } from './site-policy.page';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: CoreLoginSitePolicyPage,
+ },
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ ],
+ declarations: [
+ CoreLoginSitePolicyPage,
+ ],
+ exports: [RouterModule],
+})
+export class CoreLoginSitePolicyPageModule {}
diff --git a/src/app/core/login/pages/site-policy/site-policy.page.ts b/src/app/core/login/pages/site-policy/site-policy.page.ts
new file mode 100644
index 000000000..ca4858feb
--- /dev/null
+++ b/src/app/core/login/pages/site-policy/site-policy.page.ts
@@ -0,0 +1,139 @@
+// (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 } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { NavController } from '@ionic/angular';
+
+import { CoreSites } from '@services/sites';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreUtils } from '@services/utils/utils';
+import { CoreMimetypeUtils } from '@services/utils/mimetype';
+import { CoreLoginHelper } from '@core/login/services/helper';
+import { CoreSite } from '@classes/site';
+
+/**
+ * Page to accept a site policy.
+ */
+@Component({
+ selector: 'page-core-login-site-policy',
+ templateUrl: 'site-policy.html',
+})
+export class CoreLoginSitePolicyPage implements OnInit {
+
+ sitePolicy?: string;
+ showInline?: boolean;
+ policyLoaded?: boolean;
+ protected siteId?: string;
+ protected currentSite?: CoreSite;
+
+ constructor(
+ protected navCtrl: NavController,
+ protected route: ActivatedRoute,
+ ) {
+ }
+
+ /**
+ * Component initialized.
+ */
+ ngOnInit(): void {
+ const params = this.route.snapshot.queryParams;
+
+ this.siteId = params['siteId'];
+ this.currentSite = CoreSites.instance.getCurrentSite();
+
+ if (!this.currentSite) {
+ // Not logged in, stop.
+ this.cancel();
+
+ return;
+ }
+
+ const currentSiteId = this.currentSite.id;
+ this.siteId = this.siteId || currentSiteId;
+
+ if (this.siteId != currentSiteId || !this.currentSite.wsAvailable('core_user_agree_site_policy')) {
+ // Not current site or WS not available, stop.
+ this.cancel();
+
+ return;
+ }
+
+ this.fetchSitePolicy();
+ }
+
+ /**
+ * Fetch the site policy URL.
+ *
+ * @return Promise resolved when done.
+ */
+ protected async fetchSitePolicy(): Promise {
+ try {
+ this.sitePolicy = await CoreLoginHelper.instance.getSitePolicy(this.siteId);
+ } catch (error) {
+ CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting site policy.');
+ this.cancel();
+
+ return;
+ }
+
+ // Try to get the mime type.
+ try {
+ const mimeType = await CoreUtils.instance.getMimeTypeFromUrl(this.sitePolicy);
+
+ const extension = CoreMimetypeUtils.instance.getExtension(mimeType, this.sitePolicy);
+ this.showInline = extension == 'html' || extension == 'htm';
+ } catch (error) {
+ // Unable to get mime type, assume it's not supported.
+ this.showInline = false;
+ } finally {
+ this.policyLoaded = true;
+ }
+ }
+
+ /**
+ * Cancel.
+ *
+ * @return Promise resolved when done.
+ */
+ async cancel(): Promise {
+ await CoreUtils.instance.ignoreErrors(CoreSites.instance.logout());
+
+ await this.navCtrl.navigateRoot('/login/sites');
+ }
+
+ /**
+ * Accept the site policy.
+ *
+ * @return Promise resolved when done.
+ */
+ async accept(): Promise {
+ const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
+
+ try {
+ await CoreLoginHelper.instance.acceptSitePolicy(this.siteId);
+
+ // Success accepting, go to site initial page.
+ // Invalidate cache since some WS don't return error if site policy is not accepted.
+ await CoreUtils.instance.ignoreErrors(this.currentSite!.invalidateWsCache());
+
+ await CoreLoginHelper.instance.goToSiteInitialPage();
+ } catch (error) {
+ CoreDomUtils.instance.showErrorModalDefault(error, 'Error accepting site policy.');
+ } finally {
+ modal.dismiss();
+ }
+ }
+
+}
diff --git a/src/app/services/utils/iframe.ts b/src/app/services/utils/iframe.ts
index eae0e490b..f419f21f1 100644
--- a/src/app/services/utils/iframe.ts
+++ b/src/app/services/utils/iframe.ts
@@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { NavController } from '@ionic/angular';
import { WKUserScriptWindow, WKUserScriptInjectionTime } from 'cordova-plugin-wkuserscript';
+import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies';
import { CoreApp } from '@services/app';
import { CoreFile } from '@services/file';
@@ -476,6 +477,36 @@ export class CoreIframeUtilsProvider {
window.addEventListener('message', this.handleIframeMessage.bind(this));
}
+ /**
+ * Fix cookies for an iframe URL.
+ *
+ * @param url URL of the iframe.
+ * @return Promise resolved when done.
+ */
+ async fixIframeCookies(url: string): Promise {
+ if (!CoreApp.instance.isIOS() || !url || CoreUrlUtils.instance.isLocalFileUrl(url)) {
+ // No need to fix cookies.
+ return;
+ }
+
+ // Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView.
+ try {
+ const win = window;
+ const urlParts = CoreUrl.parse(url);
+
+ if (urlParts?.domain && win.WKWebViewCookies) {
+ await win.WKWebViewCookies.setCookie({
+ name: 'MoodleAppCookieForWKWebView',
+ value: '1',
+ domain: urlParts.domain,
+ });
+ }
+ } catch (err) {
+ // Ignore errors.
+ this.logger.error('Error setting cookie', err);
+ }
+ }
+
}
export class CoreIframeUtils extends makeSingleton(CoreIframeUtilsProvider) {}