Merge pull request #2439 from moodlehq/integration

Integration
main
Juan Leyva 2020-07-02 15:06:06 +02:00 committed by GitHub
commit fb047a6e35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 257 additions and 112 deletions

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget android-versionCode="39000" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.0.0" version="3.9.0" versionCode="39000" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget android-versionCode="39000" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.1.0" version="3.9.1" versionCode="39100" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Moodle</name>
<description>Moodle official app</description>
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
@ -241,7 +241,7 @@
<true />
</edit-config>
<edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString">
<string>3.9.0</string>
<string>3.9.1</string>
</edit-config>
<config-file parent="FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED" target="*-Info.plist">
<string>YES</string>

View File

@ -6,7 +6,7 @@
<Identity Name="3312ADB7.MoodleDesktop"
ProcessorArchitecture="x64"
Publisher="CN=33CDCDF6-1EB5-4827-9897-ED25C91A32F6"
Version="3.9.0.0" />
Version="3.9.1.0" />
<Properties>
<DisplayName>Moodle Desktop</DisplayName>
<PublisherDisplayName>Moodle Pty Ltd.</PublisherDisplayName>

View File

@ -1,6 +1,6 @@
{
"name": "moodlemobile",
"version": "3.9.0",
"version": "3.9.1",
"description": "The official app for Moodle.",
"author": {
"name": "Moodle Pty Ltd.",
@ -253,7 +253,7 @@
"category": "public.app-category.education",
"icon": "resources/desktop/icon.icns",
"target": "mas",
"bundleVersion": "3.9.0",
"bundleVersion": "3.9.1",
"extendInfo": {
"ElectronTeamID": "2NU57U5PAW"
}

View File

@ -120,8 +120,6 @@ function add_langs_to_config($langs, $config) {
function get_langfolder($lang) {
$folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang);
if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) {
echo "Cannot translate $folder, folder not found";
return false;
}
@ -173,6 +171,8 @@ function reset_translations_strings() {
function build_lang($lang, $keys) {
$langfoldername = get_langfolder($lang);
if (!$langfoldername) {
echo "Cannot translate $lang, folder not found";
return false;
}
@ -182,15 +182,18 @@ function build_lang($lang, $keys) {
$override_langfolder = false;
}
$total = count ($keys);
$total = count($keys);
$local = 0;
$string = get_translation_strings($langfoldername, 'langconfig');
$parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
$langparts = explode('-', $lang, 2);
$parentname = $langparts[0] ?? "";
$parent = "";
echo "Processing $lang";
if ($parent != "" && $parent != $lang) {
echo " ($parent)";
// Check parent language exists.
if ($parentname != $lang && get_langfolder($parentname)) {
echo " ($parentname)";
$parent = $parentname;
}
$langFile = false;
@ -247,6 +250,12 @@ function build_lang($lang, $keys) {
$translations[$key] = html_entity_decode($text);
}
if (!empty($parent)) {
$translations['core.parentlanguage'] = $parent;
} else if (isset($translations['core.parentlanguage'])) {
unset($translations['core.parentlanguage']);
}
// Sort and save.
ksort($translations);
file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
@ -275,6 +284,8 @@ function progressbar($percentage) {
function detect_lang($lang, $keys) {
$langfoldername = get_langfolder($lang);
if (!$langfoldername) {
echo "Cannot translate $lang, folder not found";
return false;
}

View File

@ -1913,7 +1913,6 @@
"core.openmodinbrowser": "Obre {{$a}} al navegador",
"core.othergroups": "Altres grups",
"core.pagea": "Pàgina {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Utilitzeu el botó de baix per pagar i inscriure-us.",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telèfon",

View File

@ -1909,7 +1909,6 @@
"core.openmodinbrowser": "Otevřít {{$a}} v prohlížeči",
"core.othergroups": "Další skupiny",
"core.pagea": "Stránka {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Pomocí tlačítka níže můžete provést platbu a během několika minut se zapsat do kurzu!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefon",

View File

@ -1752,7 +1752,6 @@
"core.openinbrowser": "Åben i browser",
"core.othergroups": "Andre grupper",
"core.pagea": "Side {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Brug knappen forneden til at betale og blive tilmeldt umiddelbart derefter.",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefon",

View File

@ -1841,7 +1841,6 @@
"core.openinbrowser": "Ανοίξτε στον περιηγητή.",
"core.othergroups": "Άλλες ομάδες",
"core.pagea": "Σελίδα {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Χρησιμοποιήστε το παρακάτω πλήκτρο για να πληρώσετε και να εγγραφείτε μέσα σε λίγα λεπτά!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Τηλέφωνο",

View File

@ -14,6 +14,7 @@
"core.listsep": ",",
"core.login.loginsteps": "For full access to this site, you first need to create an account.",
"core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.",
"core.parentlanguage": "en",
"core.paymentinstant": "Use the button below to pay and be enrolled within minutes!",
"core.settings.license": "License",
"core.strftimedate": "%B %d, %Y",

View File

@ -1557,6 +1557,7 @@
"core.errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.",
"core.errorsync": "An error occurred while synchronising. Please try again.",
"core.errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.",
"core.errorurlschemeinvalidsite": "This site URL cannot be opened in this app.",
"core.explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.",
"core.favourites": "Starred",
"core.filename": "Filename",

View File

@ -1913,7 +1913,7 @@
"core.openmodinbrowser": "Abrir {{$a}} en navegador",
"core.othergroups": "Otros grupos",
"core.pagea": "Página {{$a}}",
"core.parentlanguage": "",
"core.parentlanguage": "es",
"core.paymentinstant": "¡Utilice el botón de abajo para pagar y poder inscribirse en minutos!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Teléfono",

View File

@ -1913,7 +1913,6 @@
"core.openmodinbrowser": "Abrir {{$a}} en el navegador",
"core.othergroups": "Otros grupos",
"core.pagea": "Página {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "¡Utilice el botón de abajo para pagar y poder matricularse en minutos!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Teléfono",

View File

@ -1875,7 +1875,6 @@
"core.openinbrowser": "Ouvrir dans le navigateur",
"core.othergroups": "Autres groupes",
"core.pagea": "Page {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Le bouton ci-dessous vous permet de payer et de vous inscrire en quelques minutes !",
"core.percentagenumber": "{{$a}} %",
"core.phone": "Téléphone",

View File

@ -1521,7 +1521,6 @@
"core.online": "Online",
"core.othergroups": "Egyéb csoportok",
"core.pagea": "{{$a}} oldal",
"core.parentlanguage": "",
"core.paymentinstant": "A fizetéshez és a perceken belüli beiratkozáshoz használja az alábbi gombot!",
"core.phone": "Telefon",
"core.pictureof": "Kép",

View File

@ -1769,7 +1769,6 @@
"core.openinbrowser": "Apri nel browser",
"core.othergroups": "Altri gruppi",
"core.pagea": "Pagina {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Utilizza il pulsante sottostante per pagare ed essere iscritto in pochi minuti!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefono",

View File

@ -1717,7 +1717,6 @@
"core.openinbrowser": "ブラウザで開く",
"core.othergroups": "他のグループ",
"core.pagea": "ページ {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "下のボタンをお使いください。支払いおよび登録がすぐに完了します!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "電話",

View File

@ -1385,7 +1385,6 @@
"core.openfullimage": "전체 크기 이미지를 보려면 여기를 클릭하십시오.",
"core.openinbrowser": "브라우저에서 열기",
"core.pagea": "페이지 {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "신속하게 등록금 지불 및 등록을 마치려면 아래의 버튼을 사용하시오!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "전화",

View File

@ -1634,7 +1634,6 @@
"core.openinbrowser": "Atidaryti naršyklėje",
"core.othergroups": "Kitos grupės",
"core.pagea": "{{$a}} puslapis",
"core.parentlanguage": "",
"core.paymentinstant": "Naudokite toliau pateiktą mygtuką, kad sumokėtumėte ir būtumėte įregistruoti per kelias minutes.",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefonas",

View File

@ -1576,7 +1576,6 @@
"core.online": "På nett",
"core.othergroups": "Andre grupper",
"core.pagea": "Side {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Bruk knappen under for å betale og melde deg på kurset.",
"core.phone": "Telefon",
"core.pictureof": "Bilde av {{$a}}",

View File

@ -1809,7 +1809,7 @@
"core.openinbrowser": "Abrir no navegador",
"core.othergroups": "Outros grupos",
"core.pagea": "Página {{$a}}",
"core.parentlanguage": "",
"core.parentlanguage": "pt",
"core.paymentinstant": "Clique o botão abaixo para efetuar o pagamento e fazer a sua inscrição em poucos minutos!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Fone",

View File

@ -1913,7 +1913,6 @@
"core.openmodinbrowser": "Abrir {{$a}} no navegador",
"core.othergroups": "Outros grupos",
"core.pagea": "Página {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Use o botão abaixo para pagar e completar a inscrição!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefone",

View File

@ -1841,7 +1841,6 @@
"core.openinbrowser": "Odpri v brskalniku",
"core.othergroups": "Ostale skupine",
"core.pagea": "Stran {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "Uporabite spodnje gumbe za plačilo in vpis v nekaj minutah!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefon",

View File

@ -1700,7 +1700,6 @@
"core.openinbrowser": "Отвори у веб читачу",
"core.othergroups": "Друге групе",
"core.pagea": "Страница {{$a}}",
"core.parentlanguage": "en",
"core.paymentinstant": "Употребите дугме испод како бисте извршили уплату и уписали курс у року од неколико минута!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Телефон",

View File

@ -1700,7 +1700,6 @@
"core.openinbrowser": "Otvori u veb čitaču",
"core.othergroups": "Druge grupe",
"core.pagea": "Stranica {{$a}}",
"core.parentlanguage": "en",
"core.paymentinstant": "Upotrebite dugme ispod kako biste izvršili uplatu i upisali kurs u roku od nekoliko minuta!",
"core.percentagenumber": "{{$a}}%",
"core.phone": "Telefon",

View File

@ -1844,7 +1844,6 @@
"core.openinbrowser": "在浏览器中打开",
"core.othergroups": "其他小组",
"core.pagea": "页 {{$a}}",
"core.parentlanguage": "",
"core.paymentinstant": "点击下面的按钮便可以快速付费并加入课程!",
"core.phone": "电话",
"core.pictureof": "{{$a}}的头像",

View File

@ -1636,7 +1636,6 @@
"core.openinbrowser": "以瀏覽器開啟",
"core.othergroups": "其他群組",
"core.pagea": "第 {{$a}} 頁",
"core.parentlanguage": "",
"core.paymentinstant": "使用以下按鈕立即付款及註冊。",
"core.percentagenumber": "{{$a}}%",
"core.phone": "電話",

View File

@ -21,9 +21,9 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
/**
* Component to show a loading spinner and message while data is being loaded.
*
* It will show a spinner with a message and hide all the content until 'dataLoaded' variable is set to true.
* If 'message' and 'dynMessage' attributes aren't set, default message "Loading" is shown.
* 'message' attribute accepts hardcoded strings, variables, filters, etc. E.g. message="'core.loading' | translate".
* It will show a spinner with a message and hide all the content until 'hideUntil' variable is set to a truthy value (!!hideUntil).
* If 'message' isn't set, default message "Loading" is shown.
* 'message' attribute accepts hardcoded strings, variables, filters, etc. E.g. [message]="'core.loading' | translate".
*
* Usage:
* <core-loading [message]="loadingMessage" [hideUntil]="dataLoaded">
@ -44,7 +44,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
animations: [coreShowHideAnimation]
})
export class CoreLoadingComponent implements OnInit, OnChanges {
@Input() hideUntil: boolean; // Determine when should the contents be shown.
@Input() hideUntil: any; // Determine when should the contents be shown.
@Input() message?: string; // Message to show while loading.
@ViewChild('content') content: ElementRef;
@ -69,7 +69,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges {
}
// Add class if loaded on init.
if (this.hideUntil) {
if (!!this.hideUntil) {
this.element.classList.add('core-loading-loaded');
this.content.nativeElement.classList.add('core-loading-content');
}
@ -77,7 +77,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges {
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
if (changes.hideUntil) {
if (changes.hideUntil.currentValue === true) {
if (!!this.hideUntil) {
setTimeout(() => {
// Content is loaded so, center the spinner on the content itself.
this.element.classList.add('core-loading-loaded');
@ -96,7 +96,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges {
// Trigger the event after a timeout since the elements inside ngIf haven't been added to DOM yet.
setTimeout(() => {
this.eventsProvider.trigger(CoreEventsProvider.CORE_LOADING_CHANGED, {
loaded: changes.hideUntil.currentValue,
loaded: !!this.hideUntil,
uniqueId: this.uniqueId
});
});

View File

@ -2,8 +2,8 @@
"app_id": "com.moodle.moodlemobile",
"appname": "Moodle Mobile",
"desktopappname": "Moodle Desktop",
"versioncode": 3900,
"versionname": "3.9.0",
"versioncode": 3910,
"versionname": "3.9.1",
"cache_update_frequency_usually": 420000,
"cache_update_frequency_often": 1200000,
"cache_update_frequency_sometimes": 3600000,
@ -81,6 +81,7 @@
"siteurl": "",
"sitename": "",
"multisitesdisplay": "",
"onlyallowlistedsites": false,
"skipssoconfirmation": false,
"forcedefaultlanguage": false,
"privacypolicy": "https:\/\/moodle.net\/moodle-app-privacy\/",

View File

@ -105,4 +105,10 @@ ion-app.app-root page-core-login-site {
background: transparent;
}
}
.core-login-site-qrcode-separator {
text-align: center;
margin-top: 12px;
font-size: 1.2em;
}
}

View File

@ -34,6 +34,16 @@
<div padding>
<button ion-button block [disabled]="siteChecked && !isBrowserSSO && !credForm.valid" class="core-login-login-button">{{ 'core.login.loginbutton' | translate }}</button>
</div>
<ng-container *ngIf="showScanQR">
<div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div>
<ion-item class="core-login-site-qrcode" no-lines>
<a ion-button block color="light" margin-top icon-start text-wrap (click)="showInstructionsAndScanQR()">
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
{{ 'core.scanqr' | translate }}
</a>
</ion-item>
</ng-container>
</form>
<!-- Forgotten password button. -->

View File

@ -16,12 +16,14 @@ import { Component, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreUtils } from '@providers/utils/utils';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreLoginHelperProvider } from '../../providers/helper';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CoreConfigConstants } from '../../../../configconstants';
import { CoreCustomURLSchemes } from '@providers/urlschemes';
/**
* Page to enter the user credentials.
@ -47,6 +49,7 @@ export class CoreLoginCredentialsPage {
isBrowserSSO = false;
isFixedUrlSet = false;
showForgottenPassword = true;
showScanQR: boolean;
protected siteConfig;
protected eventThrown = false;
@ -74,6 +77,17 @@ export class CoreLoginCredentialsPage {
username: [navParams.get('username') || '', Validators.required],
password: ['', Validators.required]
});
const canScanQR = CoreUtils.instance.canScanQR();
if (canScanQR) {
if (typeof CoreConfigConstants['displayqroncredentialscreen'] == 'undefined') {
this.showScanQR = this.loginHelper.isFixedUrlSet();
} else {
this.showScanQR = !!CoreConfigConstants['displayqroncredentialscreen'];
}
} else {
this.showScanQR = false;
}
}
/**
@ -267,4 +281,46 @@ export class CoreLoginCredentialsPage {
signup(): void {
this.navCtrl.push('CoreLoginEmailSignupPage', { siteUrl: this.siteUrl });
}
/**
* Show instructions and scan QR code.
*/
showInstructionsAndScanQR(): void {
// Show some instructions first.
this.domUtils.showAlertWithOptions({
title: this.translate.instant('core.login.faqwhereisqrcode'),
message: this.translate.instant('core.login.faqwhereisqrcodeanswer',
{$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}),
buttons: [
{
text: this.translate.instant('core.cancel'),
role: 'cancel'
},
{
text: this.translate.instant('core.next'),
handler: (): void => {
this.scanQR();
}
},
],
});
}
/**
* Scan a QR code and put its text in the URL input.
*
* @return Promise resolved when done.
*/
async scanQR(): Promise<void> {
// Scan for a QR code.
const text = await CoreUtils.instance.scanQR();
if (text && CoreCustomURLSchemes.instance.isCustomURL(text)) {
try {
await CoreCustomURLSchemes.instance.handleCustomURL(text);
} catch (error) {
CoreCustomURLSchemes.instance.treatHandleCustomURLError(error);
}
}
}
}

View File

@ -14,16 +14,22 @@
<div text-center padding margin-bottom [class.hidden]="hasSites || enteredSiteUrl" class="core-login-site-logo">
<img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation">
</div>
<form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || fixedDisplay == 'select'" #siteFormEl>
<form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites || siteSelector == 'select'" #siteFormEl>
<!-- Form to input the site URL if there are no fixed sites. -->
<ng-container *ngIf="!fixedSites">
<ng-container *ngIf="siteSelector == 'url'">
<ion-item>
<ion-label stacked><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label>
<ion-input name="url" type="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard"></ion-input>
</ion-item>
</ng-container>
<ng-container *ngIf="siteSelector != 'url'">
<ion-item>
<ion-label stacked><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label>
<ion-input name="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard" (ionChange)="searchSite($event, siteForm.value.siteUrl)"></ion-input>
</ion-item>
</ng-container>
<ion-list *ngIf="!fixedSites" [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list">
<ion-list [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list">
<ion-item no-lines class="core-login-site-list-title"><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
<button ion-item *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)" [attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site">
<ion-thumbnail item-start>
@ -47,44 +53,51 @@
</div>
</ion-list>
<div *ngIf="!fixedSites && !hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div>
<div *ngIf="!hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div>
</ng-container>
</ng-container>
<!-- Pick the site from a list of fixed sites. -->
<ion-item *ngIf="fixedSites && fixedDisplay == 'select'" margin-vertical text-wrap>
<ion-item *ngIf="fixedSites && siteSelector == 'select'" margin-vertical text-wrap>
<ion-label stacked for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label>
<ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" interface="action-sheet">
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
</ion-select>
</ion-item>
<ng-container *ngIf="!fixedSites && showScanQR && !hasSites && !enteredSiteUrl">
<ion-item *ngIf="(fixedSites && siteSelector == 'select') || (!fixedSites && siteSelector == 'url')" no-lines>
<button ion-button block [disabled]="!siteForm.valid" text-wrap>{{ 'core.login.connect' | translate }}</button>
</ion-item>
</form>
<ng-container *ngIf="fixedSites">
<!-- Pick the site from a list of fixed sites. -->
<ion-list *ngIf="siteSelector == 'list' || siteSelector == 'listnourl'">
<ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
<ion-searchbar *ngIf="fixedSites.length > 4" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.login.findyoursite' | translate"></ion-searchbar>
<ion-item *ngFor="let site of filteredSites" (click)="connect($event, site.url)" [title]="site.name" detail-push text-wrap>
<h2>{{site.name}}</h2>
<p *ngIf="siteSelector == 'list'">{{site.url}}</p>
</ion-item>
</ion-list>
<!-- Display them using buttons. -->
<div *ngIf="siteSelector == 'buttons'">
<p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p>
<a *ngFor="let site of fixedSites" ion-button block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.name}}</a>
</div>
</ng-container>
<ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl">
<div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div>
<ion-item class="core-login-site-qrcode">
<a ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()">
<ion-item class="core-login-site-qrcode" no-lines>
<a ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()" text-wrap>
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
{{ 'core.scanqr' | translate }}
</a>
</ion-item>
</ng-container>
</form>
<!-- Pick the site from a list of fixed sites. -->
<ion-list *ngIf="fixedSites && (fixedDisplay == 'list' || fixedDisplay == 'listnourl')">
<ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item>
<ion-searchbar *ngIf="fixedSites.length > 4" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.login.findyoursite' | translate"></ion-searchbar>
<ion-item *ngFor="let site of filteredSites" (click)="connect($event, site.url)" [title]="site.name" detail-push text-wrap>
<h2>{{site.name}}</h2>
<p *ngIf="fixedDisplay == 'list'">{{site.url}}</p>
</ion-item>
</ion-list>
<!-- Display them using buttons. -->
<div *ngIf="fixedSites && fixedDisplay == 'buttons'">
<p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p>
<a *ngFor="let site of fixedSites" ion-button block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.name}}</a>
</div>
<!-- Help. -->
<ion-list no-lines margin-top>
<a ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none>

View File

@ -129,10 +129,4 @@ ion-app.app-root page-core-login-site {
.core-login-default-icon {
filter: grayscale(100%);
}
.core-login-site-qrcode-separator {
text-align: center;
margin-top: 12px;
font-size: 1.2em;
}
}

View File

@ -53,7 +53,7 @@ export class CoreLoginSitePage {
siteForm: FormGroup;
fixedSites: CoreLoginSiteInfo[];
filteredSites: CoreLoginSiteInfo[];
fixedDisplay = 'buttons';
siteSelector = 'sitefinder';
showKeyboard = false;
filter = '';
sites: CoreLoginSiteInfoExtended[] = [];
@ -80,17 +80,16 @@ export class CoreLoginSitePage {
protected textUtils: CoreTextUtilsProvider) {
this.showKeyboard = !!navParams.get('showKeyboard');
this.showScanQR = this.utils.canScanQR();
let url = '';
this.siteSelector = CoreConfigConstants.multisitesdisplay;
// Load fixed sites if they're set.
if (this.loginHelper.hasSeveralFixedSites()) {
this.fixedSites = <any[]> this.loginHelper.getFixedSites();
this.fixedDisplay = CoreConfigConstants.multisitesdisplay;
// Autoselect if not defined.
if (['list', 'listnourl', 'select', 'buttons'].indexOf(this.fixedDisplay) < 0) {
this.fixedDisplay = this.fixedSites.length > 8 ? 'list' : (this.fixedSites.length > 3 ? 'select' : 'buttons');
if (['list', 'listnourl', 'select', 'buttons'].indexOf(this.siteSelector) < 0) {
this.siteSelector = this.fixedSites.length > 8 ? 'list' : (this.fixedSites.length > 3 ? 'select' : 'buttons');
}
this.filteredSites = this.fixedSites;
url = this.fixedSites[0].url;
@ -103,6 +102,9 @@ export class CoreLoginSitePage {
});
}
this.showScanQR = this.utils.canScanQR() && (typeof CoreConfigConstants['displayqronsitescreen'] == 'undefined' ||
!!CoreConfigConstants['displayqronsitescreen']);
this.siteForm = fb.group({
siteUrl: [url, this.moodleUrlValidator()]
});

View File

@ -67,7 +67,8 @@ export class CoreMainMenuMorePage implements OnDestroy {
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this),
sitesProvider.getCurrentSiteId());
this.loadSiteInfo();
this.showScanQR = this.utils.canScanQR();
this.showScanQR = this.utils.canScanQR() &&
!this.sitesProvider.getCurrentSite().isFeatureDisabled('CoreMainMenuDelegate_QrReader');
}
/**

View File

@ -102,6 +102,7 @@
"errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.",
"errorsync": "An error occurred while synchronising. Please try again.",
"errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.",
"errorurlschemeinvalidsite": "This site URL cannot be opened in this app.",
"explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.",
"favourites": "Starred",
"filename": "Filename",

View File

@ -55,7 +55,9 @@ export class CoreLangProvider {
translate.onLangChange.subscribe((event: any) => {
platform.setLang(event.lang, true);
platform.setDir(this.translate.instant('core.thisdirection'), true);
const dir = this.translate.instant('core.thisdirection');
platform.setDir(dir.indexOf('rtl') != -1 ? 'rtl' : 'ltr', true);
});
}

View File

@ -29,6 +29,7 @@ import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins
import { CoreConfigConstants } from '../configconstants';
import { CoreConstants } from '@core/constants';
import { makeSingleton } from '@singletons/core.singletons';
import { CoreUrl } from '@singletons/url';
/**
* All params that can be in a custom URL scheme.
@ -166,6 +167,12 @@ export class CoreCustomURLSchemesProvider {
}
try {
const isValid = await this.isInFixedSiteUrls(data.siteUrl);
if (!isValid) {
throw this.translate.instant('core.errorurlschemeinvalidsite');
}
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
// Redirect URL must belong to the same site. Reject.
throw this.translate.instant('core.contentlinks.errorredirectothersite');
@ -540,6 +547,38 @@ export class CoreCustomURLSchemesProvider {
this.domUtils.showErrorModalDefault(error.error, this.translate.instant('core.login.invalidsite'));
}
}
/**
* Check if a site URL is one of the fixed sites for the app (in case there are fixed sites).
*
* @param siteUrl Site URL to check.
* @return Promise resolved with boolean: whether is one of the fixed sites.
*/
protected async isInFixedSiteUrls(siteUrl: string): Promise<boolean> {
if (this.loginHelper.isFixedUrlSet()) {
return CoreUrl.sameDomainAndPath(siteUrl, <string> this.loginHelper.getFixedSites());
} else if (this.loginHelper.hasSeveralFixedSites()) {
const sites = <any[]> this.loginHelper.getFixedSites();
const site = sites.find((site) => {
return CoreUrl.sameDomainAndPath(siteUrl, site.url);
});
return !!site;
} else if (CoreConfigConstants.multisitesdisplay == 'sitefinder' && CoreConfigConstants.onlyallowlistedsites) {
// Call the sites finder to validate the site.
const result = await this.sitesProvider.findSites(siteUrl.replace(/^https?\:\/\/|\.\w{2,3}\/?$/g, ''));
const site = result && result.find((site) => {
return CoreUrl.sameDomainAndPath(siteUrl, site.url);
});
return !!site;
}
return true;
}
}
/**

View File

@ -162,7 +162,7 @@ export class CoreWSProvider {
this.logger = logger.getInstance('CoreWSProvider');
platform.ready().then(() => {
if (this.appProvider.isMobile()) {
if (this.appProvider.isIOS()) {
(<any> cordova).plugin.http.setHeader('User-Agent', navigator.userAgent);
}
});

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreTextUtils } from '@providers/utils/text';
/**
* Parts contained within a url.
*/
@ -172,4 +174,27 @@ export class CoreUrl {
static removeProtocol(url: string): string {
return url.replace(/^[a-zA-Z]+:\/\//i, '');
}
/**
* Check if two URLs have the same domain and path.
*
* @param urlA First URL.
* @param urlB Second URL.
* @return Whether they have same domain and path.
*/
static sameDomainAndPath(urlA: string, urlB: string): boolean {
// Add protocol if missing, the parse function requires it.
if (!urlA.match(/^[^\/:\.\?]*:\/\//)) {
urlA = `https://${urlA}`;
}
if (!urlB.match(/^[^\/:\.\?]*:\/\//)) {
urlB = `https://${urlB}`;
}
const partsA = CoreUrl.parse(urlA);
const partsB = CoreUrl.parse(urlB);
return partsA.domain == partsB.domain &&
CoreTextUtils.instance.removeEndingSlash(partsA.path) == CoreTextUtils.instance.removeEndingSlash(partsB.path);
}
}