MOBILE-4268 core: Require details with error code

main
Noel De Martin 2023-03-07 11:28:19 +01:00
parent 6ce688e76c
commit eb0738cb0f
10 changed files with 219 additions and 122 deletions

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror';
/**
* Error returned by WS.
@ -29,10 +29,7 @@ export class CoreAjaxWSError extends CoreSiteError {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(error: any, available?: number) {
super({
message: error.message || error.error,
errorcode: error.errorcode,
});
super(getErrorOptions(error));
this.exception = error.exception;
this.warningcode = error.warningcode;
@ -40,15 +37,37 @@ export class CoreAjaxWSError extends CoreSiteError {
this.moreinfourl = error.moreinfourl;
this.debuginfo = error.debuginfo;
this.backtrace = error.backtrace;
this.available = available;
if (this.available === undefined) {
if (this.errorcode) {
this.available = this.errorcode == 'invalidrecord' ? -1 : 1;
} else {
this.available = 0;
}
}
this.available = available ?? (
this.debug
? (this.debug.code == 'invalidrecord' ? -1 : 1)
: 0
);
}
}
/**
* Get error options from unknown error instance.
*
* @param error The error.
* @returns Options
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getErrorOptions(error: any): CoreSiteErrorOptions {
const options: CoreSiteErrorOptions = {
message: error.message || error.error,
};
if ('debug' in error) {
options.debug = error.debug;
}
if ('errorcode' in error) {
options.debug = {
code: error.errorcode,
details: error.message || error.error,
};
}
return options;
}

View File

@ -20,24 +20,39 @@ import { CoreUserSupportConfig } from '@features/user/classes/support/support-co
*/
export class CoreSiteError extends CoreError {
errorcode?: string;
errorDetails?: string;
debug?: CoreSiteErrorDebug;
supportConfig?: CoreUserSupportConfig;
constructor(options: CoreSiteErrorOptions) {
super(options.message);
this.errorcode = options.errorcode;
this.errorDetails = options.errorDetails;
this.debug = options.debug;
this.supportConfig = options.supportConfig;
}
/**
* @deprecated This getter should not be called directly, but it's defined for backwards compatibility with many
* parts of the code that type errors as any and use it. We cannot rename those because the errors could also be
* CoreWSError instances which do have an "errorcode" property.
*
* @returns error code.
*/
get errorcode(): string | undefined {
return this.debug?.code;
}
}
export type CoreSiteErrorDebug = {
code: string; // Technical error code useful for technical assistance.
details: string; // Technical error details useful for technical assistance.
};
export type CoreSiteErrorOptions = {
message: string;
errorcode?: string; // Technical error code useful for technical assistance.
errorDetails?: string; // Technical error details useful for technical assistance.
// Debugging information.
debug?: CoreSiteErrorDebug;
// Configuration to use to contact site support. If this attribute is present, it means
// that the error warrants contacting support.

View File

@ -943,8 +943,10 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
throw new CoreSiteError({
supportConfig: new CoreUserAuthenticatedSupportConfig(this),
message: Translate.instant('core.siteunavailablehelp', { site: this.siteUrl }),
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }),
},
});
}

View File

@ -36,7 +36,7 @@ export class CoreErrorInfoComponent implements OnInit, OnChanges {
* @param errorCode Error code.
* @returns Component HTML.
*/
static render(errorDetails: string, errorCode?: string): string {
static render(errorDetails: string, errorCode: string): string {
const toggleId = CoreForms.uniqueId('error-info-toggle');
const errorCodeLabel = Translate.instant('core.errorcode', { errorCode });
const hideDetailsLabel = Translate.instant('core.errordetailshide');
@ -45,7 +45,7 @@ export class CoreErrorInfoComponent implements OnInit, OnChanges {
return `
<div class="core-error-info">
<input id="${toggleId}" type="checkbox" class="core-error-info--checkbox" />
${errorCode ? `<div class="core-error-info--code"><strong>${errorCodeLabel}</strong></div>` : ''}
<div class="core-error-info--code"><strong>${errorCodeLabel}</strong></div>
<div class="core-error-info--details">
<p>${errorDetails}</p>
</div>
@ -64,7 +64,7 @@ export class CoreErrorInfoComponent implements OnInit, OnChanges {
}
@Input() errorDetails!: string;
@Input() errorCode?: string;
@Input() errorCode!: string;
constructor(private element: ElementRef) {}

View File

@ -38,7 +38,7 @@ import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services
import { CoreTextUtils } from '@services/utils/text';
import { CoreForms } from '@singletons/form';
import { AlertButton } from '@ionic/core';
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror';
import { CoreUserSupport } from '@features/user/services/support';
import { CoreErrorInfoComponent } from '@components/error-info/error-info';
import { CoreUserSupportConfig } from '@features/user/classes/support/support-config';
@ -408,21 +408,19 @@ export class CoreLoginSitePage implements OnInit {
let siteExists = false;
let supportConfig: CoreUserSupportConfig | undefined = undefined;
let errorTitle: string | undefined;
let errorDetails: string | undefined;
let errorCode: string | undefined;
let debug: CoreSiteErrorDebug | undefined;
if (error instanceof CoreSiteError) {
supportConfig = error.supportConfig;
errorDetails = error.errorDetails;
errorCode = error.errorcode;
siteExists = supportConfig instanceof CoreUserGuestSupportConfig;
debug = error.debug;
}
if (error instanceof CoreLoginError) {
errorTitle = error.title;
}
if (errorDetails) {
if (debug) {
errorMessage = `<p>${errorMessage}</p><div class="core-error-info-container"></div>`;
}
@ -438,7 +436,7 @@ export class CoreLoginSitePage implements OnInit {
handler: () => CoreUserSupport.contact({
supportConfig: alertSupportConfig,
subject: Translate.instant('core.cannotconnect'),
message: `Error: ${errorCode}\n\n${errorDetails}`,
message: `Error: ${debug?.code}\n\n${debug?.details}`,
}),
}
: (
@ -458,11 +456,10 @@ export class CoreLoginSitePage implements OnInit {
buttons: buttons as AlertButton[],
});
if (errorDetails) {
// Avoid sanitizing JS.
if (debug) {
const containerElement = alertElement.querySelector('.core-error-info-container');
if (containerElement) {
containerElement.innerHTML = CoreErrorInfoComponent.render(errorDetails, errorCode);
containerElement.innerHTML = CoreErrorInfoComponent.render(debug.details, debug.code);
}
}
}

View File

@ -39,7 +39,6 @@ import { CorePushNotifications } from '@features/pushnotifications/services/push
import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value';
import { SafeHtml } from '@angular/platform-browser';
import { CoreLoginError } from '@classes/errors/loginerror';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import {
CoreSiteIdentityProvider,
@ -916,8 +915,8 @@ export class CoreLoginHelperProvider {
/**
* Show a modal warning that the credentials introduced were not correct.
*/
protected showInvalidLoginModal(error: CoreLoginError): void {
CoreDomUtils.showErrorModal(error.errorDetails ?? error.message);
protected showInvalidLoginModal(error: CoreWSError): void {
CoreDomUtils.showErrorModal(error.message);
}
/**

View File

@ -124,7 +124,10 @@ describe('Credentials page', () => {
getUserToken: () => {
throw new CoreLoginError({
message: '',
errorcode: 'invalidlogin',
debug: {
code: 'invalidlogin',
details: 'Invalid login',
},
});
},
checkSite: async () => (siteCheck),

View File

@ -66,6 +66,7 @@ import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse } from
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
import { firstValueFrom } from 'rxjs';
import { CoreHTMLClasses } from '@singletons/html-classes';
import { CoreSiteErrorDebug } from '@classes/errors/siteerror';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
@ -362,18 +363,22 @@ export class CoreSitesProvider {
if (!config.enablewebservices) {
throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, {
supportConfig: new CoreUserGuestSupportConfig(temporarySite, config),
errorcode: 'webservicesnotenabled',
errorDetails: Translate.instant('core.login.webservicesnotenabled'),
critical: true,
debug: {
code: 'webservicesnotenabled',
details: Translate.instant('core.login.webservicesnotenabled'),
},
});
}
if (!config.enablemobilewebservice) {
throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, {
supportConfig: new CoreUserGuestSupportConfig(temporarySite, config),
errorcode: 'mobileservicesnotenabled',
errorDetails: Translate.instant('core.login.mobileservicesnotenabled'),
critical: true,
debug: {
code: 'mobileservicesnotenabled',
details: Translate.instant('core.login.mobileservicesnotenabled'),
},
});
}
@ -421,14 +426,16 @@ export class CoreSitesProvider {
siteUrl: string,
error: CoreError | CoreAjaxError | CoreAjaxWSError,
): Promise<CoreLoginError> {
if (error instanceof CoreAjaxError || !('errorcode' in error)) {
if (error instanceof CoreAjaxError || (!('debug' in error) && !('errorcode' in error))) {
// The WS didn't return data, probably cannot connect.
return new CoreLoginError({
title: Translate.instant('core.cannotconnect'),
message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }),
errorcode: 'publicconfigfailed',
errorDetails: error.message || '',
critical: false, // Allow fallback to http if siteUrl uses https.
debug: {
code: 'publicconfigfailed',
details: error.message || 'Failed getting public config',
},
});
}
@ -437,28 +444,31 @@ export class CoreSitesProvider {
critical: true,
title: Translate.instant('core.cannotconnect'),
message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }),
errorcode: error.errorcode,
supportConfig: error.supportConfig,
errorDetails: error.errorDetails ?? error.message,
debug: error.debug,
};
if (error.errorcode === 'codingerror') {
if (error.debug?.code === 'codingerror') {
// This could be caused by a redirect. Check if it's the case.
const redirect = await CoreUtils.checkRedirect(siteUrl);
options.message = Translate.instant('core.siteunavailablehelp', { site: siteUrl });
if (redirect) {
options.errorcode = 'sitehasredirect';
options.errorDetails = Translate.instant('core.login.sitehasredirect');
options.critical = false; // Keep checking fallback URLs.
options.debug = {
code: 'sitehasredirect',
details: Translate.instant('core.login.sitehasredirect'),
};
}
} else if (error.errorcode === 'invalidrecord') {
} else if (error.debug?.code === 'invalidrecord') {
// WebService not found, site not supported.
options.message = Translate.instant('core.siteunavailablehelp', { site: siteUrl });
options.errorcode = 'invalidmoodleversion';
options.errorDetails = Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION });
} else if (error.errorcode === 'redirecterrordetected') {
options.debug = {
code: 'invalidmoodleversion',
details: Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
};
} else if (error.debug?.code === 'redirecterrordetected') {
options.critical = false; // Keep checking fallback URLs.
}
@ -538,16 +548,20 @@ export class CoreSitesProvider {
if (redirect) {
throw this.createCannotConnectLoginError(siteUrl, {
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
errorcode: 'sitehasredirect',
errorDetails: Translate.instant('core.login.sitehasredirect'),
debug: {
code: 'sitehasredirect',
details: Translate.instant('core.login.sitehasredirect'),
},
});
}
}
throw this.createCannotConnectLoginError(siteUrl, {
supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl),
errorcode: data.errorcode,
errorDetails: data.error,
debug: {
code: data.errorcode ?? 'loginfailed',
details: data.error ?? 'Could not get a user token in /login/token.php',
},
});
}
@ -659,23 +673,33 @@ export class CoreSitesProvider {
* @returns A promise rejected with the error info.
*/
protected async treatInvalidAppVersion(result: number, siteId?: string): Promise<never> {
let errorCode: string | undefined;
let debug: CoreSiteErrorDebug | undefined;
let errorKey: string | undefined;
let translateParams = {};
switch (result) {
case CoreSitesProvider.MOODLE_APP:
errorKey = 'core.login.connecttomoodleapp';
errorCode = 'connecttomoodleapp';
debug = {
code: 'connecttomoodleapp',
details: 'Cannot connect to app',
};
break;
case CoreSitesProvider.WORKPLACE_APP:
errorKey = 'core.login.connecttoworkplaceapp';
errorCode = 'connecttoworkplaceapp';
debug = {
code: 'connecttoworkplaceapp',
details: 'Cannot connect to app',
};
break;
default:
errorCode = 'invalidmoodleversion';
errorKey = 'core.login.invalidmoodleversion';
translateParams = { $a: CoreSite.MINIMUM_MOODLE_VERSION };
debug = {
code: 'invalidmoodleversion',
details: 'Cannot connect to app',
};
break;
}
if (siteId) {
@ -683,8 +707,8 @@ export class CoreSitesProvider {
}
throw new CoreLoginError({
debug,
message: Translate.instant(errorKey, translateParams),
errorcode: errorCode,
loggedOut: true,
});
}

View File

@ -1037,7 +1037,7 @@ export class CoreDomUtilsProvider {
if (typeof error !== 'string' && 'buttons' in error && typeof error.buttons !== 'undefined') {
alertOptions.buttons = error.buttons;
} else if (error instanceof CoreSiteError) {
if (error.errorDetails) {
if (error.debug) {
alertOptions.message = `<p>${alertOptions.message}</p><div class="core-error-info-container"></div>`;
}
@ -1051,7 +1051,7 @@ export class CoreDomUtilsProvider {
handler: () => CoreUserSupport.contact({
supportConfig,
subject: alertOptions.header,
message: `${error.errorcode}\n\n${error.errorDetails}`,
message: `${error.debug?.code}\n\n${error.debug?.details}`,
}),
});
}
@ -1061,11 +1061,11 @@ export class CoreDomUtilsProvider {
const alertElement = await this.showAlertWithOptions(alertOptions, autocloseTime);
if (error instanceof CoreSiteError && error.errorDetails) {
if (error instanceof CoreSiteError && error.debug) {
const containerElement = alertElement.querySelector('.core-error-info-container');
if (containerElement) {
containerElement.innerHTML = CoreErrorInfoComponent.render(error.errorDetails, error.errorcode);
containerElement.innerHTML = CoreErrorInfoComponent.render(error.debug.details, error.debug.code);
}
}

View File

@ -497,10 +497,12 @@ export class CoreWSProvider {
throw new CoreAjaxError({
message,
supportConfig: await CoreUserGuestSupportConfig.forSite(preSets.siteUrl),
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.serverconnection', {
debug: {
code: 'invalidresponse',
details: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
},
});
} else if (data.error) {
throw new CoreAjaxWSError(data);
@ -527,54 +529,72 @@ export class CoreWSProvider {
if (CorePlatform.isMobile()) {
switch (data.status) {
case NativeHttp.ErrorCode.SSL_EXCEPTION:
options.errorcode = 'invalidcertificate';
options.errorDetails = Translate.instant('core.certificaterror', {
options.debug = {
code: 'invalidcertificate',
details: Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Invalid certificate',
});
}),
};
break;
case NativeHttp.ErrorCode.SERVER_NOT_FOUND:
options.errorcode = 'servernotfound';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Server could not be found';
options.debug = {
code: 'servernotfound',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Server could not be found',
};
break;
case NativeHttp.ErrorCode.TIMEOUT:
options.errorcode = 'requesttimeout';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request timed out';
options.debug = {
code: 'requesttimeout',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request timed out',
};
break;
case NativeHttp.ErrorCode.UNSUPPORTED_URL:
options.errorcode = 'unsupportedurl';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Url not supported';
options.debug = {
code: 'unsupportedurl',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Url not supported',
};
break;
case NativeHttp.ErrorCode.NOT_CONNECTED:
options.errorcode = 'connectionerror';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error)
?? 'Connection error, is network available?';
options.debug = {
code: 'connectionerror',
details: CoreTextUtils.getErrorMessageFromError(data.error)
?? 'Connection error, is network available?',
};
break;
case NativeHttp.ErrorCode.ABORTED:
options.errorcode = 'requestaborted';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request aborted';
options.debug = {
code: 'requestaborted',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request aborted',
};
break;
case NativeHttp.ErrorCode.POST_PROCESSING_FAILED:
options.errorcode = 'requestprocessingfailed';
options.errorDetails = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request processing failed';
options.debug = {
code: 'requestprocessingfailed',
details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request processing failed',
};
break;
}
}
if (!options.errorcode) {
if (!options.debug) {
switch (data.status) {
case 404:
options.errorcode = 'endpointnotfound';
options.errorDetails = Translate.instant('core.ajaxendpointnotfound', {
options.debug = {
code: 'endpointnotfound',
details: Translate.instant('core.ajaxendpointnotfound', {
$a: CoreSite.MINIMUM_MOODLE_VERSION,
});
}),
};
break;
default: {
const details = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Unknown error';
options.errorcode = 'serverconnectionajax';
options.errorDetails = Translate.instant('core.serverconnection', {
options.debug = {
code: 'serverconnectionajax',
details: Translate.instant('core.serverconnection', {
details: `[Response status code: ${data.status}] ${details}`,
});
}),
};
}
break;
}
@ -716,10 +736,12 @@ export class CoreWSProvider {
if (!data) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'serverconnectionpost',
errorDetails: Translate.instant('core.serverconnection', {
debug: {
code: 'serverconnectionpost',
details: Translate.instant('core.serverconnection', {
details: Translate.instant('core.errorinvalidresponse', { method }),
}),
},
});
} else if (typeof data !== typeExpected) {
// If responseType is text an string will be returned, parse before returning.
@ -730,8 +752,10 @@ export class CoreWSProvider {
this.logger.warn(`Response expected type "${typeExpected}" cannot be parsed to number`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
} else if (typeExpected === 'boolean') {
@ -743,24 +767,30 @@ export class CoreWSProvider {
this.logger.warn(`Response expected type "${typeExpected}" is not true or false`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
} else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${typeExpected}"`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
} else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${typeExpected}"`);
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method }),
},
});
}
}
@ -803,10 +833,12 @@ export class CoreWSProvider {
return retryPromise;
} else if (error.status === -2) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidcertificate',
errorDetails: Translate.instant('core.certificaterror', {
debug: {
code: 'invalidcertificate',
details: Translate.instant('core.certificaterror', {
details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error',
}),
},
});
} else if (error.status > 0) {
throw this.createHttpError(error, error.status);
@ -1033,24 +1065,30 @@ export class CoreWSProvider {
if (data === null) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
},
});
}
if (!data) {
throw await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'serverconnectionupload',
errorDetails: Translate.instant('core.serverconnection', {
debug: {
code: 'serverconnectionupload',
details: 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 await this.createCannotConnectSiteError(preSets.siteUrl, {
errorcode: 'invalidresponse',
errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
debug: {
code: 'invalidresponse',
details: Translate.instant('core.errorinvalidresponse', { method: 'upload.php' }),
},
});
}