diff --git a/src/config.json b/src/config.json
index 7b6e032bd..0a6c014e4 100644
--- a/src/config.json
+++ b/src/config.json
@@ -81,6 +81,7 @@
"siteurl": "",
"sitename": "",
"multisitesdisplay": "",
+ "onlyallowlistedsites": false,
"skipssoconfirmation": false,
"forcedefaultlanguage": false,
"privacypolicy": "https:\/\/moodle.net\/moodle-app-privacy\/",
@@ -104,4 +105,4 @@
"mac": "id1255924440",
"linux": "https:\/\/download.moodle.org\/desktop\/download.php?platform=linux&arch=64"
}
-}
\ No newline at end of file
+}
diff --git a/src/core/login/login.scss b/src/core/login/login.scss
index 5b5ba112a..8206b3750 100644
--- a/src/core/login/login.scss
+++ b/src/core/login/login.scss
@@ -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;
+ }
}
diff --git a/src/core/login/pages/credentials/credentials.html b/src/core/login/pages/credentials/credentials.html
index eb2eab7ed..749997711 100644
--- a/src/core/login/pages/credentials/credentials.html
+++ b/src/core/login/pages/credentials/credentials.html
@@ -34,6 +34,16 @@
+
+
+ {{ 'core.login.or' | translate }}
+
+
+
+ {{ 'core.scanqr' | translate }}
+
+
+
diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts
index e614d1b8b..180aa315e 100644
--- a/src/core/login/pages/credentials/credentials.ts
+++ b/src/core/login/pages/credentials/credentials.ts
@@ -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 {
+ // 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);
+ }
+ }
+ }
}
diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html
index d5dc5c1d9..c466aa73a 100644
--- a/src/core/login/pages/site/site.html
+++ b/src/core/login/pages/site/site.html
@@ -56,17 +56,6 @@
{{site.name}}
-
-
- {{ 'core.login.or' | translate }}
-
-
-
- {{ 'core.scanqr' | translate }}
-
-
-
-
@@ -85,6 +74,16 @@
{{site.name}}
+
+ {{ 'core.login.or' | translate }}
+
+
+
+ {{ 'core.scanqr' | translate }}
+
+
+
+
diff --git a/src/core/login/pages/site/site.scss b/src/core/login/pages/site/site.scss
index cff2ef02e..bde235732 100644
--- a/src/core/login/pages/site/site.scss
+++ b/src/core/login/pages/site/site.scss
@@ -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;
- }
}
diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts
index d21809dd5..9845898cc 100644
--- a/src/core/login/pages/site/site.ts
+++ b/src/core/login/pages/site/site.ts
@@ -80,7 +80,6 @@ export class CoreLoginSitePage {
protected textUtils: CoreTextUtilsProvider) {
this.showKeyboard = !!navParams.get('showKeyboard');
- this.showScanQR = this.utils.canScanQR();
let 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()]
});
diff --git a/src/lang/en.json b/src/lang/en.json
index 64b9a3172..68341442e 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -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",
diff --git a/src/providers/urlschemes.ts b/src/providers/urlschemes.ts
index 577b9d05b..20e466e81 100644
--- a/src/providers/urlschemes.ts
+++ b/src/providers/urlschemes.ts
@@ -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 {
+ if (this.loginHelper.isFixedUrlSet()) {
+
+ return CoreUrl.sameDomainAndPath(siteUrl, this.loginHelper.getFixedSites());
+ } else if (this.loginHelper.hasSeveralFixedSites()) {
+ const sites = 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;
+ }
}
/**
diff --git a/src/singletons/url.ts b/src/singletons/url.ts
index cd1798c79..019e893c2 100644
--- a/src/singletons/url.ts
+++ b/src/singletons/url.ts
@@ -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);
+ }
}