Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"siteurl": "",
"sitename": "",
"multisitesdisplay": "",
"onlyallowlistedsites": false,
"skipssoconfirmation": false,
"forcedefaultlanguage": false,
"privacypolicy": "https:\/\/moodle.net\/moodle-app-privacy\/",
Expand All @@ -104,4 +105,4 @@
"mac": "id1255924440",
"linux": "https:\/\/download.moodle.org\/desktop\/download.php?platform=linux&arch=64"
}
}
}
6 changes: 6 additions & 0 deletions src/core/login/login.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
10 changes: 10 additions & 0 deletions src/core/login/pages/credentials/credentials.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ <h3 *ngIf="siteName" padding class="core-sitename"><core-format-text [text]="sit
<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">
<a ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()">
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
{{ 'core.scanqr' | translate }}
</a>
</ion-item>
</ng-container>
</form>

<!-- Forgotten password button. -->
Expand Down
56 changes: 56 additions & 0 deletions src/core/login/pages/credentials/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -47,6 +49,7 @@ export class CoreLoginCredentialsPage {
isBrowserSSO = false;
isFixedUrlSet = false;
showForgottenPassword = true;
showScanQR: boolean;

protected siteConfig;
protected eventThrown = false;
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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);
}
}
}
}
21 changes: 10 additions & 11 deletions src/core/login/pages/site/site.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,6 @@ <h2 text-wrap>{{site.name}}<ng-container *ngIf="site.alias"> ({{site.alias}})</n
<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">
<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()">
<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. -->
Expand All @@ -85,6 +74,16 @@ <h2>{{site.name}}</h2>
<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 *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()">
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
{{ 'core.scanqr' | translate }}
</a>
</ion-item>
</ng-container>

<!-- Help. -->
<ion-list no-lines margin-top>
<a ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none>
Expand Down
6 changes: 0 additions & 6 deletions src/core/login/pages/site/site.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
4 changes: 3 additions & 1 deletion src/core/login/pages/site/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export class CoreLoginSitePage {
protected textUtils: CoreTextUtilsProvider) {

this.showKeyboard = !!navParams.get('showKeyboard');
this.showScanQR = this.utils.canScanQR();

let url = '';

Expand All @@ -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()]
});
Expand Down
1 change: 1 addition & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 39 additions & 0 deletions src/providers/urlschemes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/singletons/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
}
}