Skip to content
Draft
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
50 changes: 49 additions & 1 deletion moodle.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,53 @@
}
},
"clearIABSessionWhenAutoLogin": "android",
"collapsibleItemsExpanded": false
"collapsibleItemsExpanded": false,
"wsOverrides": {
"tool_mobile_get_config": [
{
"op": "replace",
"path": "/settings/[name=tool_mobile_disabledfeatures]/value",
"userid": 15,
"value": "CoreReportBuilderDelegate,CoreMainMenuDelegate_AddonBlog,CoreMainMenuDelegate_CoreTag,CoreMainMenuDelegate_CoreGrades,CoreUserDelegate_AddonBadges:account,CoreUserDelegate_AddonBlog:account,CoreCourseOptionsDelegate_AddonCompetency,CoreMainMenuDelegate_CoreSiteHome"
},
{
"op": "replace",
"path": "/fullname",
"userid": 3108,
"value": "Dani Test"
},
{
"op": "replace",
"path": "/fullname",
"userid": 15,
"value": "Dani Test"
},
{
"op": "replace",
"path": "/aaa",
"userid": 15,
"value": "Dani Test"
},
{
"op": "replace",
"path": "/bbb",
"userid": 15,
"value": "Dani Test"
}
],
"tool_mobile_get_public_config": [
{
"op": "replace",
"path": "/settings/[name=tool_mobile_disabledfeatures]/value",
"value": "CoreReportBuilderDelegate,CoreMainMenuDelegate_AddonBlog,CoreMainMenuDelegate_CoreTag,CoreMainMenuDelegate_CoreGrades,CoreUserDelegate_AddonBadges:account,CoreUserDelegate_AddonBlog:account,CoreCourseOptionsDelegate_AddonCompetency,CoreMainMenuDelegate_CoreSiteHome"
}
],
"core_badges_get_user_badge_by_hash": [
{
"op": "replace",
"path": "/badge/imageurl",
"value": "https://moodle.net/wp-content/uploads/2020/05/moodle-badge.png"
}
]
}
}
70 changes: 61 additions & 9 deletions src/core/classes/sites/authenticated-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject
import { finalize, map, mergeMap } from 'rxjs/operators';
import { CoreSiteError } from '@classes/errors/siteerror';
import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config';
import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from './unauthenticated-site';
import {
CoreSiteInfo,
CoreSiteInfoResponse,
CoreSitePublicConfigResponse,
CoreUnauthenticatedSite,
CoreWSOverride,
} from './unauthenticated-site';
import { Md5 } from 'ts-md5';
import { CoreSiteWSCacheRecord } from '@services/database/sites';
import { CoreErrorLogs } from '@singletons/error-logs';
Expand Down Expand Up @@ -100,7 +106,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
privateToken?: string;
infos?: CoreSiteInfo;

protected logger: CoreLogger;
protected logger = CoreLogger.getInstance('CoreAuthenticatedSite');
protected cleanUnicode = false;
protected offlineDisabled = false;
private memoryCache: Record<string, CoreSiteWSCacheRecord> = {};
Expand All @@ -124,7 +130,6 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
) {
super(siteUrl, otherData.publicConfig);

this.logger = CoreLogger.getInstance('CoreAuthenticaedSite');
this.token = token;
this.privateToken = otherData.privateToken;
}
Expand Down Expand Up @@ -465,8 +470,13 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
}

const observable = this.performRequest<T>(method, data, preSets, wsPreSets).pipe(
// Return a clone of the original object, this may prevent errors if in the callback the object is modified.
map((data) => CoreUtils.clone(data)),
map((data) => {
// Always clone the object because it can be modified when applying patches or in the caller function
// and we don't want to store the modified object in cache.
const clonedData = CoreUtils.clone(data);

return this.applyWSOverrides(method, clonedData);
}),
);

this.setOngoingRequest(cacheId, preSets, observable);
Expand Down Expand Up @@ -1347,13 +1357,13 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
* @inheritdoc
*/
async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise<CoreSitePublicConfigResponse> {
const method = 'tool_mobile_get_public_config';
const ignoreCache = options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK ||
options.readingStrategy === CoreSitesReadingStrategy.PREFER_NETWORK;
if (!ignoreCache && this.publicConfig) {
return this.publicConfig;
return this.overridePublicConfig(this.publicConfig);
}

const method = 'tool_mobile_get_public_config';
const cacheId = this.getCacheId(method, {});
const cachePreSets: CoreSiteWSPreSets = {
getFromCache: true,
Expand All @@ -1378,8 +1388,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {

const subject = new Subject<CoreSitePublicConfigResponse>();
const observable = subject.pipe(
// Return a clone of the original object, this may prevent errors if in the callback the object is modified.
map((data) => CoreUtils.clone(data)),
map((data) => this.overridePublicConfig(data)),
finalize(() => {
this.clearOngoingRequest(cacheId, cachePreSets, observable);
}),
Expand Down Expand Up @@ -1613,6 +1622,49 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
CoreEvents.trigger(eventName, data);
}

/**
* @inheritdoc
*/
protected shouldApplyWSOverride(method: string, data: unknown, patch: CoreWSOverride): boolean {
if (!Number(patch.userid)) {
return true;
}

const info = this.infos ?? (method === 'core_webservice_get_site_info' ? (data as CoreSiteInfoResponse) : undefined);

if (!info?.userid) {
// Strange case, when doing WS calls the site should always have the userid already.
// Apply the patch to match the behaviour of unauthenticated site.
return true;
}

return Number(patch.userid) === info.userid;
}

/**
* Get the list of applicable WS overrides for this site.
*
* @returns WS overrides that should be applied for this site.
*/
getApplicableWSOverrides(): Record<string, CoreWSOverride[]> {
if (!CoreConstants.CONFIG.wsOverrides) {
return {};
}

const effectiveOverrides: Record<string, CoreWSOverride[]> = {};

Object.keys(CoreConstants.CONFIG.wsOverrides).forEach((method) => {
const appliedPatches = CoreConstants.CONFIG.wsOverrides![method].filter((patch) =>
this.shouldApplyWSOverride(method, {}, patch));

if (appliedPatches.length) {
effectiveOverrides[method] = appliedPatches;
}
});

return effectiveOverrides;
}

}

/**
Expand Down
95 changes: 92 additions & 3 deletions src/core/classes/sites/unauthenticated-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ import { CoreText } from '@singletons/text';
import { CoreUrl, CoreUrlPartNames } from '@singletons/url';
import { CoreWS, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws';
import { CorePath } from '@singletons/path';
import { CoreJsonPatch, JsonPatchOperation } from '@singletons/json-patch';
import { CoreUtils } from '@singletons/utils';
import { CoreLogger } from '@singletons/logger';

/**
* Class that represents a Moodle site where the user still hasn't authenticated.
*/
export class CoreUnauthenticatedSite {

protected logger = CoreLogger.getInstance('CoreUnauthenticatedSite');

siteUrl: string;

protected publicConfig?: CoreSitePublicConfigResponse;
Expand Down Expand Up @@ -251,7 +256,7 @@ export class CoreUnauthenticatedSite {
const ignoreCache = options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK ||
options.readingStrategy === CoreSitesReadingStrategy.PREFER_NETWORK;
if (!ignoreCache && this.publicConfig) {
return this.publicConfig;
return this.overridePublicConfig(this.publicConfig);
}

if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_CACHE) {
Expand All @@ -263,7 +268,7 @@ export class CoreUnauthenticatedSite {

this.setPublicConfig(config);

return config;
return this.overridePublicConfig(config);
} catch (error) {
if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK || !this.publicConfig) {
throw error;
Expand All @@ -284,6 +289,20 @@ export class CoreUnauthenticatedSite {
this.publicConfig = publicConfig;
}

/**
* Apply overrides to the public config of the site.
*
* @param config Public config.
* @returns Public config with overrides if any.
*/
protected overridePublicConfig(config: CoreSitePublicConfigResponse): CoreSitePublicConfigResponse {
// Always clone the object because it can be modified when applying patches or in the caller function
// and we don't want to modify the stored public config.
const clonedData = CoreUtils.clone(config);

return this.applyWSOverrides('tool_mobile_get_public_config', clonedData);
}

/**
* Perform a request to the server to get the public config of this site.
*
Expand Down Expand Up @@ -413,7 +432,7 @@ export class CoreUnauthenticatedSite {
*
* @returns Disabled features.
*/
protected getDisabledFeatures(): string {
getDisabledFeatures(): string {
const siteDisabledFeatures = this.getSiteDisabledFeatures() || undefined; // If empty string, use undefined.
const appDisabledFeatures = CoreConstants.CONFIG.disabledFeatures;

Expand Down Expand Up @@ -452,6 +471,69 @@ export class CoreUnauthenticatedSite {
return features;
}

/**
* Call a Moodle WS using the AJAX API and applies WebService overrides (if any) to the result.
*
* @param method WS method name.
* @param data Arguments to pass to the method.
* @param preSets Extra settings and information.
* @returns Promise resolved with the response data in success and rejected with CoreAjaxError.
*/
async callAjax<T = unknown>(
method: string,
data: Record<string, unknown> = {},
preSets: Omit<CoreWSAjaxPreSets, 'siteUrl'> = {},
): Promise<T> {
const result = await CoreWS.callAjax<T>(method, data, { ...preSets, siteUrl: this.siteUrl });

// No need to clone the data in this case because it's not stored in any cache.
return this.applyWSOverrides(method, result);
}

/**
* Apply WS overrides (if any) to the data of a WebService response.
*
* @param method WS method name.
* @param data WS response data.
* @returns Modified data (or original data if no overrides).
*/
protected applyWSOverrides<T>(method: string, data: T): T {
if (!CoreConstants.CONFIG.wsOverrides || !CoreConstants.CONFIG.wsOverrides[method]) {
return data;
}

CoreConstants.CONFIG.wsOverrides[method].forEach((patch) => {
if (!this.shouldApplyWSOverride(method, data, patch)) {
this.logger.warn('Patch ignored, conditions not fulfilled:', method, patch);

return;
}

try {
CoreJsonPatch.applyPatch(data, patch);
} catch (error) {
this.logger.error('Error applying WS override:', error, patch);
}
});

return data;
}

/**
* Whether a patch should be applied as a WS override.
*
* @param method WS method name.
* @param data Data returned by the WS.
* @param patch Patch to check.
* @returns Whether it should be applied.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected shouldApplyWSOverride(method: string, data: unknown, patch: CoreWSOverride): boolean {
// Always apply patches for unauthenticated sites since we don't have user info.
// If the pacth for an AJAX WebService contains an userid is probably by mistake.
return true;
}

}

/**
Expand Down Expand Up @@ -603,3 +685,10 @@ export enum TypeOfLogin {
BROWSER = 2, // SSO in browser window is required.
EMBEDDED = 3, // SSO in embedded browser is required.
}

/**
* WebService override patch.
*/
export type CoreWSOverride = JsonPatchOperation & {
userid?: number; // To apply the patch only if the current user matches this userid.
};
11 changes: 4 additions & 7 deletions src/core/features/login/pages/email-signup/email-signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Component, ElementRef, OnInit, ChangeDetectorRef, inject, viewChild } f
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { CoreText } from '@singletons/text';
import { CoreCountries, CoreCountry } from '@singletons/countries';
import { CoreWS, CoreWSExternalWarning } from '@services/ws';
import { CoreWSExternalWarning } from '@services/ws';
import { Translate } from '@singletons';
import { CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from '@classes/sites/unauthenticated-site';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
Expand Down Expand Up @@ -197,10 +197,8 @@ export default class CoreLoginEmailSignupPage implements OnInit {
if (this.ageDigitalConsentVerification === undefined) {

const result = await CorePromiseUtils.ignoreErrors(
CoreWS.callAjax<IsAgeVerificationEnabledWSResponse>(
this.site.callAjax<IsAgeVerificationEnabledWSResponse>(
'core_auth_is_age_digital_consent_verification_enabled',
{},
{ siteUrl: this.site.getURL() },
),
);

Expand Down Expand Up @@ -344,10 +342,9 @@ export default class CoreLoginEmailSignupPage implements OnInit {
this.signupForm.value,
);

const result = await CoreWS.callAjax<SignupUserWSResult>(
const result = await this.site.callAjax<SignupUserWSResult>(
'auth_email_signup_user',
params,
{ siteUrl: this.site.getURL() },
);

if (result.success) {
Expand Down Expand Up @@ -430,7 +427,7 @@ export default class CoreLoginEmailSignupPage implements OnInit {
params.age = parseInt(params.age, 10); // Use just the integer part.

try {
const result = await CoreWS.callAjax<IsMinorWSResult>('core_auth_is_minor', params, { siteUrl: this.site.getURL() });
const result = await this.site.callAjax<IsMinorWSResult>('core_auth_is_minor', params);

CoreForms.triggerFormSubmittedEvent(this.ageFormElement(), true);

Expand Down
Loading