Merge pull request #4083 from dpalou/MOBILE-4604

Mobile 4604
main
Pau Ferrer Ocaña 2024-06-05 13:11:30 +02:00 committed by GitHub
commit 1b2f160d8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 97 additions and 32 deletions

View File

@ -133,5 +133,6 @@
"moodle" "moodle"
] ]
} }
} },
"clearIABSessionWhenAutoLogin": "android"
} }

View File

@ -19,12 +19,12 @@
<ion-list *ngIf="url && !shouldIframe && (!shouldEmbed || !isOther)"> <ion-list *ngIf="url && !shouldIframe && (!shouldEmbed || !isOther)">
<ion-item *ngIf="shouldEmbed"> <ion-item *ngIf="shouldEmbed">
<ion-label> <ion-label>
<img *ngIf="isImage" [alt]="name" [src]="url"> <img *ngIf="isImage" [alt]="name" [src]="embeddedUrl">
<video *ngIf="isVideo" [title]="name" controls controlsList="nodownload"> <video *ngIf="isVideo" [title]="name" controls controlsList="nodownload">
<source [src]="url" [type]="mimetype"> <source [src]="embeddedUrl" [type]="mimetype">
</video> </video>
<audio *ngIf="isAudio" [title]="name" controls> <audio *ngIf="isAudio" [title]="name" controls>
<source [src]="url" [type]="mimetype"> <source [src]="embeddedUrl" [type]="mimetype">
</audio> </audio>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@ -23,6 +23,7 @@ import { CoreTextUtils } from '@services/utils/text';
import { AddonModUrl, AddonModUrlDisplayOptions, AddonModUrlUrl } from '../../services/url'; import { AddonModUrl, AddonModUrlDisplayOptions, AddonModUrlUrl } from '../../services/url';
import { AddonModUrlHelper } from '../../services/url-helper'; import { AddonModUrlHelper } from '../../services/url-helper';
import { ADDON_MOD_URL_COMPONENT } from '../../constants'; import { ADDON_MOD_URL_COMPONENT } from '../../constants';
import { CoreSites } from '@services/sites';
/** /**
* Component that displays a url. * Component that displays a url.
@ -38,6 +39,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
pluginName = 'url'; pluginName = 'url';
url?: string; url?: string;
embeddedUrl?: string;
name?: string; name?: string;
shouldEmbed = false; shouldEmbed = false;
shouldIframe = false; shouldIframe = false;
@ -137,15 +139,22 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
this.shouldEmbed = displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED; this.shouldEmbed = displayType == CoreConstants.RESOURCELIB_DISPLAY_EMBED;
this.shouldIframe = displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME; this.shouldIframe = displayType == CoreConstants.RESOURCELIB_DISPLAY_FRAME;
if (this.shouldEmbed) { if (!this.shouldEmbed) {
const extension = CoreMimetypeUtils.guessExtensionFromUrl(url.externalurl); return;
this.mimetype = CoreMimetypeUtils.getMimeType(extension);
this.isImage = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_image']);
this.isAudio = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_audio']);
this.isVideo = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_video']);
this.isOther = !this.isImage && !this.isAudio && !this.isVideo;
} }
const extension = CoreMimetypeUtils.guessExtensionFromUrl(url.externalurl);
this.mimetype = CoreMimetypeUtils.getMimeType(extension);
this.isImage = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_image']);
this.isAudio = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_audio']);
this.isVideo = CoreMimetypeUtils.isExtensionInGroup(extension, ['web_video']);
this.isOther = !this.isImage && !this.isAudio && !this.isVideo;
// Fix the URL if it uses pluginfile endpoint.
const currentSite = CoreSites.getCurrentSite();
this.embeddedUrl = currentSite && this.url ?
await currentSite.checkAndFixPluginfileURL(this.url) : '';
} }
/** /**

View File

@ -54,6 +54,7 @@ import { CoreFilepool } from '@services/filepool';
import { CoreSiteInfo } from './unauthenticated-site'; import { CoreSiteInfo } from './unauthenticated-site';
import { CoreAuthenticatedSite, CoreAuthenticatedSiteOptionalData, CoreSiteWSPreSets, WSObservable } from './authenticated-site'; import { CoreAuthenticatedSite, CoreAuthenticatedSiteOptionalData, CoreSiteWSPreSets, WSObservable } from './authenticated-site';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { CorePlatform } from '@services/platform';
/** /**
* Class that represents a site (combination of site + user). * Class that represents a site (combination of site + user).
@ -557,6 +558,17 @@ export class CoreSite extends CoreAuthenticatedSite {
// Open the URL. // Open the URL.
if (inApp) { if (inApp) {
if (
options.clearsessioncache === undefined && autoLoginUrl !== url &&
(
CoreConstants.CONFIG.clearIABSessionWhenAutoLogin === 'all' ||
(CoreConstants.CONFIG.clearIABSessionWhenAutoLogin === 'android' && CorePlatform.isAndroid()) ||
(CoreConstants.CONFIG.clearIABSessionWhenAutoLogin === 'ios' && CorePlatform.isIOS())
)
) {
options.clearsessioncache = 'yes';
}
return CoreUtils.openInApp(autoLoginUrl, options); return CoreUtils.openInApp(autoLoginUrl, options);
} else { } else {
return CoreUtils.openInBrowser(autoLoginUrl, options); return CoreUtils.openInBrowser(autoLoginUrl, options);

View File

@ -23,7 +23,7 @@ import {
EventEmitter, EventEmitter,
OnDestroy, OnDestroy,
} from '@angular/core'; } from '@angular/core';
import { CoreFile } from '@services/file'; import { CoreFile, CoreFileProvider } from '@services/file';
import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool'; import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
@ -41,6 +41,9 @@ import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreArray } from '@singletons/array'; import { CoreArray } from '@singletons/array';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
import { CoreWS } from '@services/ws';
/** /**
* Directive to handle external content. * Directive to handle external content.
@ -142,13 +145,12 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
* Get the URL that should be handled and, if valid, handle it. * Get the URL that should be handled and, if valid, handle it.
*/ */
protected async checkAndHandleExternalContent(): Promise<void> { protected async checkAndHandleExternalContent(): Promise<void> {
const siteId = this.siteId || CoreSites.getRequiredCurrentSite().getId();
const tagName = this.element.tagName.toUpperCase(); const tagName = this.element.tagName.toUpperCase();
let targetAttr: string; let targetAttr: string;
let url: string; let url: string;
// Always handle inline styles (if any). // Always handle inline styles (if any).
this.handleInlineStyles(siteId); this.handleInlineStyles(this.siteId);
if (tagName === 'A' || tagName == 'IMAGE') { if (tagName === 'A' || tagName == 'IMAGE') {
targetAttr = 'href'; targetAttr = 'href';
@ -165,7 +167,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
if (tagName === 'VIDEO' && (this.posterUrl || this.poster)) { // eslint-disable-line deprecation/deprecation if (tagName === 'VIDEO' && (this.posterUrl || this.poster)) { // eslint-disable-line deprecation/deprecation
// Handle poster. // Handle poster.
// eslint-disable-next-line deprecation/deprecation // eslint-disable-next-line deprecation/deprecation
this.handleExternalContent('poster', this.posterUrl ?? this.poster ?? '', siteId).catch(() => { this.handleExternalContent('poster', this.posterUrl ?? this.poster ?? '').catch(() => {
// Ignore errors. // Ignore errors.
}); });
} }
@ -178,7 +180,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
} }
try { try {
await this.handleExternalContent(targetAttr, url, siteId); await this.handleExternalContent(targetAttr, url);
} catch (error) { } catch (error) {
// Error handling content. Make sure the original URL is set. // Error handling content. Make sure the original URL is set.
this.setElementUrl(targetAttr, url); this.setElementUrl(targetAttr, url);
@ -192,28 +194,27 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
* *
* @param targetAttr Attribute to modify. * @param targetAttr Attribute to modify.
* @param url Original URL to treat. * @param url Original URL to treat.
* @param siteId Site ID.
* @returns Promise resolved if the element is successfully treated. * @returns Promise resolved if the element is successfully treated.
*/ */
protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<void> { protected async handleExternalContent(targetAttr: string, url: string): Promise<void> {
const tagName = this.element.tagName; const tagName = this.element.tagName;
if (tagName == 'VIDEO' && targetAttr != 'poster') { if (tagName == 'VIDEO' && targetAttr != 'poster') {
this.handleVideoSubtitles(<HTMLVideoElement> this.element); this.handleVideoSubtitles(<HTMLVideoElement> this.element);
} }
const site = await CoreSites.getSite(siteId); const site = await CoreUtils.ignoreErrors(CoreSites.getSite(this.siteId));
const isSiteFile = site.isSitePluginFileUrl(url); const isSiteFile = site?.isSitePluginFileUrl(url);
if (!url || !url.match(/^https?:\/\//i) || CoreUrlUtils.isLocalFileUrl(url) || if (!url || !url.match(/^https?:\/\//i) || CoreUrlUtils.isLocalFileUrl(url) ||
(tagName === 'A' && !(isSiteFile || site.isSiteThemeImageUrl(url) || CoreUrlUtils.isGravatarUrl(url)))) { (tagName === 'A' && !(isSiteFile || site?.isSiteThemeImageUrl(url) || CoreUrlUtils.isGravatarUrl(url)))) {
this.logger.debug('Ignoring non-downloadable URL: ' + url); this.logger.debug('Ignoring non-downloadable URL: ' + url);
throw new CoreError('Non-downloadable URL'); throw new CoreError('Non-downloadable URL');
} }
if (!site.canDownloadFiles() && isSiteFile) { if (site && !site.canDownloadFiles() && isSiteFile) {
this.element.parentElement?.removeChild(this.element); // Remove element since it'll be broken. this.element.parentElement?.removeChild(this.element); // Remove element since it'll be broken.
throw new CoreError(Translate.instant('core.cannotdownloadfiles')); throw new CoreError(Translate.instant('core.cannotdownloadfiles'));
@ -225,7 +226,9 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
this.setElementUrl(targetAttr, finalUrl); this.setElementUrl(targetAttr, finalUrl);
this.setListeners(targetAttr, url, site); if (site) {
this.setListeners(targetAttr, url, site);
}
} }
/** /**
@ -350,7 +353,11 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
* @param site Site. * @param site Site.
* @returns Promise resolved with the URL. * @returns Promise resolved with the URL.
*/ */
protected async getUrlToUse(targetAttr: string, url: string, site: CoreSite): Promise<string> { protected async getUrlToUse(targetAttr: string, url: string, site?: CoreSite): Promise<string> {
if (!site) {
return this.getUrlForNoSite(url);
}
const tagName = this.element.tagName; const tagName = this.element.tagName;
let finalUrl: string; let finalUrl: string;
@ -397,6 +404,36 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
return finalUrl; return finalUrl;
} }
/**
* Get the URL to use when there's no site (the user hasn't authenticated yet).
* In Android, the file will always be downloaded and served from the local file system to avoid problems with cookies.
* In other platforms the file will never be downloaded, we'll always use the online URL.
*
* @param url Original URL to treat.
* @returns Promise resolved with the URL.
*/
protected async getUrlForNoSite(url: string): Promise<string> {
if (!CorePlatform.isAndroid()) {
return url;
}
const fileId = CoreFilepool.getFileIdByUrl(url);
const extension = CoreMimetypeUtils.guessExtensionFromUrl(url);
const filePath = CoreFileProvider.NO_SITE_FOLDER + '/' + fileId + (extension ? '.' + extension : '');
let fileEntry: FileEntry;
try {
// Check if the file is already downloaded.
fileEntry = await CoreFile.getFile(filePath);
} catch {
// File not downloaded, download it first.
fileEntry = await CoreWS.downloadFile(url, filePath, false);
}
return CoreFile.convertFileSrc(CoreFile.getFileEntryURL(fileEntry));
}
/** /**
* Set listeners if needed. * Set listeners if needed.
* *

View File

@ -81,7 +81,8 @@
</ion-badge> </ion-badge>
</ion-label> </ion-label>
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="end" [attr.aria-label]="grade.iconAlt" /> <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="end" [attr.aria-label]="grade.iconAlt" />
<img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image" slot="end" [alt]="grade.iconAlt" /> <img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image" slot="end" [alt]="grade.iconAlt"
core-external-content />
<ion-icon *ngIf="grade.image && grade.itemmodule" name="fas-chart-bar" slot="end" <ion-icon *ngIf="grade.image && grade.itemmodule" name="fas-chart-bar" slot="end"
[attr.aria-label]="grade.iconAlt" /> [attr.aria-label]="grade.iconAlt" />
</ion-item> </ion-item>

View File

@ -43,7 +43,7 @@
class="expandable-status-icon" [class.expandable-status-icon-expanded]="row.expanded" /> class="expandable-status-icon" [class.expandable-status-icon-expanded]="row.expanded" />
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt" /> <ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt" />
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon" <img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
[alt]="row.iconAlt" /> [alt]="row.iconAlt" core-external-content />
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start" <core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start"
[modname]="row.itemmodule" [colorize]="false" /> [modname]="row.itemmodule" [colorize]="false" />
<span [innerHTML]="row.gradeitem"></span> <span [innerHTML]="row.gradeitem"></span>

View File

@ -25,7 +25,8 @@
<div class="core-login-site"> <div class="core-login-site">
<div class="core-login-site-logo"> <div class="core-login-site-logo">
<!-- Show site logo or a default image. --> <!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" role="presentation" alt="" onError="this.src='assets/img/login_logo.png'"> <img *ngIf="logoUrl" [src]="logoUrl" role="presentation" alt="" core-external-content
onError="this.src='assets/img/login_logo.png'">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt=""> <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt="">
</div> </div>

View File

@ -33,7 +33,8 @@
<div class="core-login-site"> <div class="core-login-site">
<div class="core-login-site-logo" *ngIf="!showUserAvatar"> <div class="core-login-site-logo" *ngIf="!showUserAvatar">
<!-- Show site logo or a default image. --> <!-- Show site logo or a default image. -->
<img *ngIf="logoUrl" [src]="logoUrl" role="presentation" onError="this.src='assets/img/login_logo.png'" alt=""> <img *ngIf="logoUrl" [src]="logoUrl" role="presentation" onError="this.src='assets/img/login_logo.png'" alt=""
core-external-content [siteId]="siteId">
<img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt=""> <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation" alt="">
</div> </div>

View File

@ -115,7 +115,8 @@
<ng-template #sitelisting let-site="site"> <ng-template #sitelisting let-site="site">
<ion-item button (click)="connect(site.url, $event)" [ngClass]="site.className" [attr.aria-label]="site.name" [detail]="true"> <ion-item button (click)="connect(site.url, $event)" [ngClass]="site.className" [attr.aria-label]="site.name" [detail]="true">
<ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start"> <ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start">
<img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'" alt="" role="presentation"> <img [src]="site.imageurl" *ngIf="site.imageurl" core-external-content onError="this.src='assets/icon/icon.png'" alt=""
role="presentation">
<img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon" alt="" role="presentation"> <img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon" alt="" role="presentation">
</ion-thumbnail> </ion-thumbnail>
<ion-label> <ion-label>

View File

@ -21,7 +21,7 @@
<ion-label> <ion-label>
<!-- Show site logo. --> <!-- Show site logo. -->
<img class="core-usermenu-site-logo" *ngIf="siteLogo && siteLogoLoaded" [src]="siteLogo" role="presentation" alt="" <img class="core-usermenu-site-logo" *ngIf="siteLogo && siteLogoLoaded" [src]="siteLogo" role="presentation" alt=""
onError="this.class='image-not-found'"> core-external-content onError="this.class='image-not-found'">
<p class="core-usermenu-sitename"> <p class="core-usermenu-sitename">
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true" /> <core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true" />
</p> </p>

View File

@ -75,6 +75,7 @@ export class CoreFileProvider {
// Folders. // Folders.
static readonly SITESFOLDER = 'sites'; static readonly SITESFOLDER = 'sites';
static readonly TMPFOLDER = 'tmp'; static readonly TMPFOLDER = 'tmp';
static readonly NO_SITE_FOLDER = 'nosite';
static readonly CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native. static readonly CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native.

View File

@ -1341,7 +1341,7 @@ export class CoreFilepoolProvider {
* @param fileUrl The absolute URL to the file. * @param fileUrl The absolute URL to the file.
* @returns The file ID. * @returns The file ID.
*/ */
protected getFileIdByUrl(fileUrl: string): string { getFileIdByUrl(fileUrl: string): string {
let url = fileUrl; let url = fileUrl;
// If site supports it, since 3.8 we use tokenpluginfile instead of pluginfile. // If site supports it, since 3.8 we use tokenpluginfile instead of pluginfile.

View File

@ -76,4 +76,5 @@ export interface EnvironmentConfig {
demoMode?: boolean; // Whether to run the app in "demo mode". demoMode?: boolean; // Whether to run the app in "demo mode".
hideInformativeLinks?: boolean; // Whether to hide informative links. hideInformativeLinks?: boolean; // Whether to hide informative links.
iconsPrefixes?: Record<string, Record<string, string[]>>; // Prefixes for custom font icons (located in src/assets/fonts). iconsPrefixes?: Record<string, Record<string, string[]>>; // Prefixes for custom font icons (located in src/assets/fonts).
clearIABSessionWhenAutoLogin?: 'android' | 'ios' | 'all'; // Clear the session every time a new IAB is opened with auto-login.
} }