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) {}