MOBILE-4059 core: Improve errors mentioning admins

main
Noel De Martin 2022-10-06 16:33:16 +02:00
parent dca3f76296
commit dabd4e8046
12 changed files with 213 additions and 108 deletions

View File

@ -1919,8 +1919,6 @@
"core.login.connecttomoodle": "local_moodlemobileapp",
"core.login.connecttomoodleapp": "local_moodlemobileapp",
"core.login.connecttoworkplaceapp": "local_moodlemobileapp",
"core.login.contactyouradministrator": "local_moodlemobileapp",
"core.login.contactyouradministratorissue": "local_moodlemobileapp",
"core.login.createaccount": "moodle",
"core.login.createuserandpass": "moodle",
"core.login.credentialsdescription": "local_moodlemobileapp",
@ -1955,7 +1953,6 @@
"core.login.forcepasswordchangenotice": "moodle",
"core.login.forgotten": "moodle",
"core.login.help": "moodle",
"core.login.helpmelogin": "local_moodlemobileapp",
"core.login.instructions": "auth",
"core.login.invalidaccount": "local_moodlemobileapp",
"core.login.invaliddate": "calendar/errorinvaliddate",
@ -2398,7 +2395,6 @@
"core.weeks": "moodle",
"core.whatisyourage": "moodle",
"core.wheredoyoulive": "moodle",
"core.whoissiteadmin": "local_moodlemobileapp",
"core.whyisthishappening": "local_moodlemobileapp",
"core.whyisthisrequired": "moodle",
"core.wsfunctionnotavailable": "local_moodlemobileapp",

View File

@ -15,7 +15,7 @@
"errordownloadscorm": "Error downloading SCORM: \"{{name}}\".",
"errorgetscorm": "Error getting SCORM data.",
"errorinvalidversion": "Sorry, the application only supports SCORM 1.2.",
"errornotdownloadable": "The download of SCORM packages is disabled. Please contact your site administrator.",
"errornotdownloadable": "This institution has disabled the download of SCORM packages.",
"errornovalidsco": "This SCORM package doesn't have a visible SCO to load.",
"errorpackagefile": "Sorry, the application only supports ZIP packages.",
"errorsyncscorm": "An error occurred while synchronising. Please try again.",
@ -49,4 +49,4 @@
"toc": "TOC",
"warningofflinedatadeleted": "Some offline data from attempt {{number}} has been discarded because it couldn't be counted as a new attempt.",
"warningsynconlineincomplete": "Some attempts couldn't be synchronised with the site because the last online attempt is not yet finished. Please finish the online attempt first."
}
}

View File

@ -12,15 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreError } from '@classes/errors/error';
import { CoreSiteError } from '@classes/errors/siteerror';
/**
* Error returned by WS.
*/
export class CoreAjaxWSError extends CoreError {
export class CoreAjaxWSError extends CoreSiteError {
exception?: string; // Name of the Moodle exception.
errorcode?: string;
warningcode?: string;
link?: string; // Link to the site.
moreinfourl?: string; // Link to a page with more info.
@ -30,10 +29,12 @@ export class CoreAjaxWSError extends CoreError {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(error: any, available?: number) {
super(error.message || error.error);
super({
message: error.message || error.error,
errorcode: error.errorcode,
});
this.exception = error.exception;
this.errorcode = error.errorcode;
this.warningcode = error.warningcode;
this.link = error.link;
this.moreinfourl = error.moreinfourl;

View File

@ -61,6 +61,7 @@ import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject
import { finalize, map, mergeMap } from 'rxjs/operators';
import { firstValueFrom } from '../utils/rxjs';
import { CoreUserSupport } from '@features/user/services/support';
import { CoreSiteError } from '@classes/errors/siteerror';
/**
* QR Code type enumeration.
@ -1157,7 +1158,18 @@ export class CoreSite {
);
if (!data || !data.responses) {
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
const siteConfig = await CoreUtils.ignoreErrors(
this.getPublicConfig({ readingStrategy: CoreSitesReadingStrategy.ONLY_CACHE }),
);
throw new CoreSiteError({
siteConfig,
contactSupport: true,
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }),
});
}
requests.forEach((request, i) => {

View File

@ -39,7 +39,7 @@
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<p>{{ 'core.login.faqcannotconnectanswer' | translate }} {{ 'core.whoissiteadmin' | translate }}</p>
<p>{{ 'core.login.faqcannotconnectanswer' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">

View File

@ -14,13 +14,11 @@
"connecttomoodle": "Connect to Moodle",
"connecttomoodleapp": "You are trying to connect to a regular Moodle site. Please download the official Moodle app to access this site.",
"connecttoworkplaceapp": "You are trying to connect to a Moodle Workplace site. Please download the Moodle Workplace app to access this site.",
"contactyouradministrator": "Contact your site administrator for further help.",
"contactyouradministratorissue": "Please ask your site administrator to check the following issue: {{$a}}",
"createaccount": "Create my new account",
"createuserandpass": "Choose your username and password",
"credentialsdescription": "Please provide your username and password to log in.",
"emailconfirmsent": "<p>An email should have been sent to your address at <b>{{$a}}</b></p>\n <p>It contains easy instructions to complete your registration.</p>\n <p>If you continue to have difficulty, contact the site administrator.</p>",
"emailconfirmsentnoemail": "<p>An email should have been sent to your address.</p><p>It contains easy instructions to complete your registration.</p><p>If you continue to have difficulty, contact the site administrator.</p>",
"emailconfirmsent": "<p>An email should have been sent to your address at <b>{{$a}}</b></p><p>It contains easy instructions to complete your registration.</p>",
"emailconfirmsentnoemail": "<p>An email should have been sent to your address.</p><p>It contains easy instructions to complete your registration.</p>",
"emailconfirmsentsuccess": "Confirmation email sent successfully",
"emailnotmatch": "Emails do not match",
"erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium",
@ -32,7 +30,7 @@
"exceededloginattemptssupportsubject": "I can't log in",
"exceededpasswordresetattempts": "It seems you are having trouble accessing your account. You can contact your administrator or try again later.",
"exceededpasswordresetattemptssupportsubject": "I can't reset my password",
"faqcannotconnectanswer": "Please, contact your site administrator.",
"faqcannotconnectanswer": "Please, contact your institution.",
"faqcannotconnectquestion": "I typed my site address correctly but I still can't connect.",
"faqcannotfindmysiteanswer": "Have you typed the name correctly? It's also possible that your site is not included in our public sites directory. If you still can't find it, please enter your site address instead.",
"faqcannotfindmysitequestion": "I can't find my site.",
@ -50,12 +48,11 @@
"forcepasswordchangenotice": "You must change your password to proceed.",
"forgotten": "Forgotten your username or password?",
"help": "Help",
"helpmelogin": "<p>There are many thousands of Moodle sites around the world. This app can only connect to Moodle sites that have specifically enabled Mobile app access.</p><p>If you can't connect to your Moodle site then you need to contact your site administrator and ask them to read <a href=\"http://docs.moodle.org/en/Mobile_app\" target=\"_blank\">http://docs.moodle.org/en/Mobile_app</a></p><p>To test the app in a Moodle demo site type <i>teacher</i> or <i>student</i> in the <i>Site address</i> field and click the <b>Connect button</b>.</p>",
"instructions": "Instructions",
"invalidaccount": "Please check your login details or ask your site administrator to check the site configuration.",
"invalidaccount": "Please check your login details and try again.",
"invaliddate": "Invalid date",
"invalidemail": "Invalid email address",
"invalidmoodleversion": "<p>Invalid Moodle site version. The Moodle app only supports Moodle systems {{$a}} onwards.</p>\n<p>You can contact your site administrators and ask them to update their Moodle system.</p>\n<p>\"Site Administrators\" are the people who manages the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.</p>",
"invalidmoodleversion": "Invalid Moodle site version. The Moodle app only supports Moodle systems {{$a}} onwards.",
"invalidsite": "The site URL is invalid.",
"invalidtime": "Invalid time",
"invalidurl": "Invalid URL specified",
@ -69,7 +66,7 @@
"missingemail": "Missing email address",
"missingfirstname": "Missing given name",
"missinglastname": "Missing surname",
"mobileservicesnotenabled": "Mobile access is not enabled on your site. Please contact your site administrator if you think it should be enabled.",
"mobileservicesnotenabled": "Mobile services are not enabled on the site.",
"mustconfirm": "You need to confirm your account",
"newaccount": "New account",
"notloggedin": "You need to be logged in.",
@ -127,7 +124,7 @@
"usernamerequired": "Username required",
"usernotaddederror": "User not added - error",
"visitchangepassword": "Do you want to visit the site to change the password?",
"webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.",
"webservicesnotenabled": "Web services are not enabled on the site.",
"youcanstillconnectwithcredentials": "You can still connect to the site by entering your username and password.",
"yourenteredsite": "Connect to your site"
}

View File

@ -25,6 +25,26 @@ describe('Credentials page', () => {
// Arrange.
const siteUrl = 'https://campus.example.edu';
mockSingleton(CoreSites, {
getPublicSiteConfigByUrl: async () => ({
wwwroot: 'https://campus.example.edu',
httpswwwroot: 'https://campus.example.edu',
sitename: 'Example Campus',
guestlogin: 0,
rememberusername: 0,
authloginviaemail: 0,
registerauth: '',
forgottenpasswordurl: '',
authinstructions: '',
authnoneenabled: 0,
enablewebservices: 1,
enablemobilewebservice: 1,
maintenanceenabled: 0,
maintenancemessage: '',
typeoflogin: 1,
}),
});
// Act.
const fixture = await renderPageComponent(CoreLoginCredentialsPage, {
routeParams: { siteUrl },

View File

@ -15,6 +15,7 @@
import { Input, OnInit, ElementRef, Directive } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content';
@ -72,7 +73,12 @@ export class CoreSitePluginsCallWSOnClickBaseDirective extends CoreSitePluginsCa
await super.callWS();
} catch (error) {
if (this.showError === undefined || CoreUtils.isTrueOrOne(this.showError)) {
CoreDomUtils.showErrorModalDefault(error, 'core.serverconnection', true);
CoreDomUtils.showErrorModalDefault(
error,
Translate.instant('core.serverconnection', {
details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
}),
);
}
} finally {
modal.dismiss();

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreApp } from '@services/app';
import { CoreCustomURLSchemes } from '@services/urlschemes';
@ -45,10 +46,12 @@ export default function(): void {
if (isExternalApp && url.includes('://token=')) {
// It's an SSO token for another app. Close the IAB and show an error.
CoreUtils.closeInAppBrowser();
CoreDomUtils.showErrorModal(Translate.instant('core.login.contactyouradministratorissue', {
$a: '<br><br>' + Translate.instant('core.errorurlschemeinvalidscheme', {
$a: urlScheme,
}),
CoreDomUtils.showErrorModal(new CoreSiteError({
contactSupport: true,
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
errorcode: 'invalidurlscheme',
errorDetails: Translate.instant('core.errorurlschemeinvalidscheme', { $a: urlScheme }),
}));
return;

View File

@ -3,7 +3,7 @@
"add": "Add",
"agelocationverification": "Age and location verification",
"ago": "{{$a}} ago",
"ajaxendpointnotfound": "<p>AJAX endpoint not found. This can happen if the Moodle site is too old or it blocks access to this endpoint. The Moodle app only supports Moodle systems {{$a}} onwards. Please contact your site administrator.</p>\n<p>{{whoisadmin}}</p>",
"ajaxendpointnotfound": "AJAX endpoint not found. This can happen if the Moodle site is too old or it blocks access to this endpoint. The Moodle app only supports Moodle systems {{$a}} onwards.",
"all": "All",
"allgroups": "All groups",
"allparticipants": "All participants",
@ -18,7 +18,7 @@
"cannotconnecttrouble": "We're having trouble connecting to your site.",
"cannotconnecttroublewithoutsupport": "We're having trouble connecting to your site, please contact your institution.",
"cannotconnectverify": "<strong>Please check the address is correct.</strong>",
"cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.",
"cannotdownloadfiles": "This institution has disabled downloading files.",
"cannotinstallapk": "For security reasons, you can't install unknown apps on your device from this app. Please open the file using a browser.",
"cannotlogoutpageblocks": "Please save or discard your changes before continuing.",
"cannotopeninapp": "This file may not work as expected on this device. Would you like to open it anyway?",
@ -28,7 +28,7 @@
"captureimage": "Take picture",
"capturevideo": "Record video",
"category": "Category",
"certificaterror": "<p>The certificate of this site cannot be trusted by your device. Please contact your site administrator.</p>\n<p>{{whoisadmin}}</p>",
"certificaterror": "The certificate of this site cannot be trusted by your device: {{details}}",
"choose": "Choose",
"choosedots": "Choose...",
"clearsearch": "Clear search",
@ -110,7 +110,7 @@
"errordownloadingsomefiles": "Error downloading files. Some files might be missing.",
"errorfileexistssamename": "A file with this name already exists.",
"errorinvalidform": "The form contains invalid data. Please check that all required fields are filled in and that the data is valid.",
"errorinvalidresponse": "Invalid response received. Please contact your site administrator if the error persists.",
"errorinvalidresponse": "Invalid response received for {{method}} webservice.",
"errorloadingcontent": "Error loading content.",
"errorofflinedisabled": "Offline browsing is disabled on your site. You need to be connected to the internet to use the app.",
"erroropenfiledownloading": "Error opening file: you need to wait for the download to complete.",
@ -275,7 +275,7 @@
"selectagroup": "Select a group",
"send": "Send",
"sending": "Sending",
"serverconnection": "Error connecting to the server",
"serverconnection": "Error connecting to the server: {{details}}",
"show": "Show",
"showadvanced": "Show advanced",
"showless": "Show less...",
@ -353,7 +353,6 @@
"weeks": "weeks",
"whatisyourage": "What is your age?",
"wheredoyoulive": "In which country do you live?",
"whoissiteadmin": "\"Site Administrators\" are the people who manage the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.",
"whyisthishappening": "Why is this happening?",
"whyisthisrequired": "Why is this required?",
"wsfunctionnotavailable": "The web service function is not available.",

View File

@ -296,14 +296,14 @@ export class CoreSitesProvider {
// Check that the user can authenticate.
if (!config.enablewebservices) {
throw this.createCannotConnectError({
throw this.createCannotConnectLoginError({
errorcode: 'webservicesnotenabled',
errorDetails: Translate.instant('core.login.webservicesnotenabled'),
siteConfig: config,
critical: true,
});
} else if (!config.enablemobilewebservice) {
throw this.createCannotConnectError({
throw this.createCannotConnectLoginError({
errorcode: 'mobileservicesnotenabled',
errorDetails: Translate.instant('core.login.mobileservicesnotenabled'),
siteConfig: config,
@ -327,18 +327,29 @@ export class CoreSitesProvider {
}
/**
* Create an error to be thrown when it isn't possible to connect to a site.
* Create an error to be thrown when it isn't possible to login to a site.
*
* @param options Error options.
* @return Cannot connect error.
*/
protected createCannotConnectError(options: Partial<CoreLoginErrorOptions> = {}): CoreLoginError {
return new CoreLoginError({
protected createCannotConnectLoginError(options?: Partial<CoreLoginErrorOptions>): CoreLoginError;
protected createCannotConnectLoginError(siteUrl: string, options?: Partial<CoreLoginErrorOptions>): Promise<CoreLoginError>;
protected createCannotConnectLoginError(
siteUrlOrOptions: string | Partial<CoreLoginErrorOptions> = {},
options: Partial<CoreLoginErrorOptions> = {},
): CoreLoginError | Promise<CoreLoginError> {
const createError = (options: Partial<CoreLoginErrorOptions>) => new CoreLoginError({
...options,
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
contactSupport: true,
});
return typeof siteUrlOrOptions === 'object'
? createError(siteUrlOrOptions)
: CoreUtils
.ignoreErrors(this.getPublicSiteConfigByUrl(siteUrlOrOptions))
.then(siteConfig => createError({ ...options, siteConfig }));
}
/**
@ -361,31 +372,40 @@ export class CoreSitesProvider {
}
// Service supported but an error happened. Return error.
let critical = true;
const options: CoreLoginErrorOptions = {
critical: true,
message: error.message,
errorcode: error.errorcode,
contactSupport: error.contactSupport,
siteConfig: error.siteConfig,
errorDetails: error.errorDetails,
};
if (error.errorcode === 'codingerror') {
// This could be caused by a redirect. Check if it's the case.
const redirect = await CoreUtils.checkRedirect(siteUrl);
if (redirect) {
error.message = Translate.instant('core.login.sitehasredirect');
critical = false; // Keep checking fallback URLs.
options.message = Translate.instant('core.cannotconnecttrouble');
options.fallbackMessage = Translate.instant('core.cannotconnecttroublewithoutsupport');
options.errorcode = 'sitehasredirect';
options.errorDetails = Translate.instant('core.login.sitehasredirect');
options.critical = false; // Keep checking fallback URLs.
} else {
// We can't be sure if there is a redirect or not. Display cannot connect error.
error.message = Translate.instant('core.cannotconnecttrouble');
options.message = Translate.instant('core.cannotconnecttrouble');
}
} else if (error.errorcode === 'invalidrecord') {
// WebService not found, site not supported.
error.message = Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION });
options.message = Translate.instant('core.cannotconnecttrouble');
options.fallbackMessage = Translate.instant('core.cannotconnecttroublewithoutsupport');
options.errorcode = 'invalidmoodleversion';
options.errorDetails = Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION });
} else if (error.errorcode === 'redirecterrordetected') {
critical = false; // Keep checking fallback URLs.
options.critical = false; // Keep checking fallback URLs.
}
return new CoreLoginError({
message: error.message,
errorcode: error.errorcode,
critical,
});
return new CoreLoginError(options);
}
/**
@ -405,33 +425,31 @@ export class CoreSitesProvider {
data = await Http.post(siteUrl + '/login/token.php', { appsitecheck: 1 }).pipe(timeout(CoreWS.getRequestTimeout()))
.toPromise();
} catch (error) {
// Default error messages are kinda bad, return our own message.
throw new CoreLoginError({
message: Translate.instant('core.cannotconnecttrouble'),
throw await this.createCannotConnectLoginError(siteUrl, {
errorcode: 'sitecheckfailed',
errorDetails: CoreDomUtils.getErrorMessage(error) ?? undefined,
});
}
if (data === null) {
// Cannot connect.
throw new CoreLoginError({
message: Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
throw await this.createCannotConnectLoginError(siteUrl, {
errorcode: 'appsitecheckfailed',
errorDetails: 'A request to /login/token.php with appsitecheck=1 returned an empty response',
});
}
if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) {
throw new CoreLoginError({
throw await this.createCannotConnectLoginError(siteUrl, {
errorcode: data.errorcode,
message: data.error ?? '',
errorDetails: data.error,
});
}
if (data.error && data.error == 'Web services must be enabled in Advanced features.') {
const siteConfig = await CoreUtils.ignoreErrors(this.getPublicSiteConfigByUrl(siteUrl));
throw this.createCannotConnectError({
throw await this.createCannotConnectLoginError(siteUrl, {
errorcode: 'enablewsdescription',
errorDetails: data.error,
siteConfig,
});
}
@ -491,15 +509,16 @@ export class CoreSitesProvider {
const redirect = await CoreUtils.checkRedirect(loginUrl);
if (redirect) {
throw new CoreLoginError({
message: Translate.instant('core.login.sitehasredirect'),
throw await this.createCannotConnectLoginError(siteUrl, {
errorcode: 'sitehasredirect',
errorDetails: Translate.instant('core.login.sitehasredirect'),
});
}
}
throw new CoreLoginError({
message: data.error,
throw await this.createCannotConnectLoginError(siteUrl, {
errorcode: data.errorcode,
errorDetails: data.error,
});
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { HttpResponse, HttpParams } from '@angular/common/http';
import { HttpResponse, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { FileEntry } from '@ionic-native/file/ngx';
import { FileUploadOptions, FileUploadResult } from '@ionic-native/file-transfer/ngx';
@ -41,6 +41,7 @@ import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform';
import { CoreUtils } from '@services/utils/utils';
import { CoreSites } from '@services/sites';
import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror';
/**
* This service allows performing WS calls and download/upload files.
@ -477,7 +478,9 @@ export class CoreWSProvider {
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.serverconnection'),
errorDetails: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
});
} else if (data.error) {
throw new CoreAjaxWSError(data);
@ -491,24 +494,34 @@ export class CoreWSProvider {
}
return data.data;
}, (data) => {
let message = '';
}, async (data: HttpErrorResponse) => {
const options: CoreSiteErrorOptions = {
contactSupport: true,
siteConfig: await CoreUtils.ignoreErrors(CoreSites.getPublicSiteConfigByUrl(preSets.siteUrl)),
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
};
switch (data.status) {
case -2: // Certificate error.
message = this.getCertificateErrorMessage(data.error);
break;
case 404: // AJAX endpoint not found.
message = Translate.instant('core.ajaxendpointnotfound', {
$a: CoreSite.MINIMUM_MOODLE_VERSION,
whoisadmin: Translate.instant('core.whoissiteadmin'),
options.errorcode = 'invalidcertificate';
options.errorDetails = Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Unknown error',
});
break;
case 404: // AJAX endpoint not found.
options.errorcode = 'endpointnotfound';
options.errorDetails = Translate.instant('core.ajaxendpointnotfound', { $a: CoreSite.MINIMUM_MOODLE_VERSION });
break;
default:
message = Translate.instant('core.serverconnection');
options.errorcode = 'serverconnectionajax';
options.errorDetails = Translate.instant('core.serverconnection', {
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Unknown error',
});
break;
}
throw new CoreAjaxError(message, 1, data.status);
throw new CoreAjaxError(options, 1, data.status);
});
}
@ -630,7 +643,7 @@ export class CoreWSProvider {
const promise = Http.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return promise.then((data: any) => {
return promise.then(async (data: any) => {
// Some moodle web services return null.
// If the responseExpected value is set to false, we create a blank object if the response is null.
if (!data && !preSets.responseExpected) {
@ -638,7 +651,12 @@ export class CoreWSProvider {
}
if (!data) {
throw new CoreError(Translate.instant('core.serverconnection'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'serverconnectionpost',
errorDetails: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
});
} else if (typeof data != preSets.typeExpected) {
// If responseType is text an string will be returned, parse before returning.
if (typeof data == 'string') {
@ -647,7 +665,10 @@ export class CoreWSProvider {
if (isNaN(data)) {
this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`);
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
});
}
} else if (preSets.typeExpected == 'boolean') {
if (data === 'true') {
@ -657,17 +678,26 @@ export class CoreWSProvider {
} else {
this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`);
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
});
}
} else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
});
}
} else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
});
}
}
@ -685,7 +715,7 @@ export class CoreWSProvider {
}
return data;
}, (error) => {
}, async (error) => {
// If server has heavy load, retry after some seconds.
if (error.status == 429) {
const retryPromise = this.addToRetryQueue<T>(method, siteUrl, ajaxData, preSets);
@ -708,7 +738,12 @@ export class CoreWSProvider {
return retryPromise;
} else if (error.status === -2) {
throw new CoreError(this.getCertificateErrorMessage(error.error));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidcertificate',
errorDetails: Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
}),
});
} else if (error.status > 0) {
throw this.createHttpError(error, error.status);
}
@ -717,24 +752,6 @@ export class CoreWSProvider {
});
}
/**
* Get error message about certificate error.
*
* @param error Exact error message.
* @return Certificate error message.
*/
protected getCertificateErrorMessage(error?: string): string {
const message = Translate.instant('core.certificaterror', {
whoisadmin: Translate.instant('core.whoissiteadmin'),
});
if (error) {
return `${message}\n<p>${error}</p>`;
}
return message;
}
/**
* Retry all requests in the queue.
* This function uses recursion in order to add a delay between requests to reduce stress.
@ -850,10 +867,12 @@ export class CoreWSProvider {
}
if (!data) {
throw new CoreError(Translate.instant('core.serverconnection'));
throw new CoreError(Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}));
} else if (typeof data != preSets.typeExpected) {
this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"');
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw new CoreError(Translate.instant('core.errorinvalidresponse', { method }));
}
if (data.exception !== undefined || data.debuginfo !== undefined) {
@ -923,15 +942,26 @@ export class CoreWSProvider {
);
if (data === null) {
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
});
}
if (!data) {
throw new CoreError(Translate.instant('core.serverconnection'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'serverconnectionupload',
errorDetails: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
}),
});
} else if (typeof data != 'object') {
this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"');
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
});
}
if (data.exception !== undefined) {
@ -1091,6 +1121,28 @@ export class CoreWSProvider {
}
}
/**
* Create an error to be thrown when it isn't possible to connect to a site.
*
* @param siteUrl Site url.
* @param options Error options.
* @return Cannot connect error.
*/
protected async createCannotConnectSiteError(
siteUrl: string,
options?: Partial<CoreSiteErrorOptions>,
): Promise<CoreSiteError> {
const siteConfig = await CoreUtils.ignoreErrors(CoreSites.getPublicSiteConfigByUrl(siteUrl));
return new CoreSiteError({
...options,
siteConfig,
message: Translate.instant('core.cannotconnecttrouble'),
fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'),
contactSupport: true,
});
}
}
export const CoreWS = makeSingleton(CoreWSProvider);