Skip to content

Commit 2296f7a

Browse files
authored
Merge pull request #4596 from dpalou/MOBILE-4919
MOBILE-4919 core: Allow overriding WS response via config
2 parents a002e8f + 1e0bc58 commit 2296f7a

File tree

13 files changed

+590
-51
lines changed

13 files changed

+590
-51
lines changed

.github/workflows/testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
cat circular-dependencies
6969
lines=$(cat circular-dependencies | wc -l)
7070
echo "Total circular dependencies: $lines"
71-
test $lines -eq 81
71+
test $lines -eq 80
7272
- name: JavaScript code compatibility
7373
run: |
7474
# Check for ES2021 features, allowing ErrorCause feature.

src/core/classes/sites/authenticated-site.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject
3737
import { finalize, map, mergeMap } from 'rxjs/operators';
3838
import { CoreSiteError } from '@classes/errors/siteerror';
3939
import { CoreUserAuthenticatedSupportConfig } from '@features/user/classes/support/authenticated-support-config';
40-
import { CoreSiteInfo, CoreSiteInfoResponse, CoreSitePublicConfigResponse, CoreUnauthenticatedSite } from './unauthenticated-site';
40+
import {
41+
CoreSiteInfo,
42+
CoreSiteInfoResponse,
43+
CoreSitePublicConfigResponse,
44+
CoreUnauthenticatedSite,
45+
CoreWSOverride,
46+
} from './unauthenticated-site';
4147
import { Md5 } from 'ts-md5';
4248
import { CoreSiteWSCacheRecord } from '@services/database/sites';
4349
import { CoreErrorLogs } from '@singletons/error-logs';
@@ -101,7 +107,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
101107
privateToken?: string;
102108
infos?: CoreSiteInfo;
103109

104-
protected logger: CoreLogger;
110+
protected logger = CoreLogger.getInstance('CoreAuthenticatedSite');
105111
protected cleanUnicode = false;
106112
protected offlineDisabled = false;
107113
private memoryCache: Record<string, CoreSiteWSCacheRecord> = {};
@@ -125,7 +131,6 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
125131
) {
126132
super(siteUrl, otherData.publicConfig);
127133

128-
this.logger = CoreLogger.getInstance('CoreAuthenticaedSite');
129134
this.token = token;
130135
this.privateToken = otherData.privateToken;
131136
}
@@ -466,8 +471,13 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
466471
}
467472

468473
const observable = this.performRequest<T>(method, data, preSets, wsPreSets).pipe(
469-
// Return a clone of the original object, this may prevent errors if in the callback the object is modified.
470-
map((data) => CoreUtils.clone(data)),
474+
map((data) => {
475+
// Always clone the object because it can be modified when applying patches or in the caller function
476+
// and we don't want to store the modified object in cache.
477+
const clonedData = CoreUtils.clone(data);
478+
479+
return this.applyWSOverrides(method, clonedData);
480+
}),
471481
);
472482

473483
this.setOngoingRequest(cacheId, preSets, observable);
@@ -1361,13 +1371,13 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
13611371
* @inheritdoc
13621372
*/
13631373
async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise<CoreSitePublicConfigResponse> {
1374+
const method = 'tool_mobile_get_public_config';
13641375
const ignoreCache = options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK ||
13651376
options.readingStrategy === CoreSitesReadingStrategy.PREFER_NETWORK;
13661377
if (!ignoreCache && this.publicConfig) {
1367-
return this.publicConfig;
1378+
return this.overridePublicConfig(this.publicConfig);
13681379
}
13691380

1370-
const method = 'tool_mobile_get_public_config';
13711381
const cacheId = this.getCacheId(method, {});
13721382
const cachePreSets: CoreSiteWSPreSets = {
13731383
getFromCache: true,
@@ -1392,8 +1402,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
13921402

13931403
const subject = new Subject<CoreSitePublicConfigResponse>();
13941404
const observable = subject.pipe(
1395-
// Return a clone of the original object, this may prevent errors if in the callback the object is modified.
1396-
map((data) => CoreUtils.clone(data)),
1405+
map((data) => this.overridePublicConfig(data)),
13971406
finalize(() => {
13981407
this.clearOngoingRequest(cacheId, cachePreSets, observable);
13991408
}),
@@ -1627,6 +1636,49 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
16271636
CoreEvents.trigger(eventName, data);
16281637
}
16291638

1639+
/**
1640+
* @inheritdoc
1641+
*/
1642+
protected shouldApplyWSOverride(method: string, data: unknown, patch: CoreWSOverride): boolean {
1643+
if (!Number(patch.userid)) {
1644+
return true;
1645+
}
1646+
1647+
const info = this.infos ?? (method === 'core_webservice_get_site_info' ? (data as CoreSiteInfoResponse) : undefined);
1648+
1649+
if (!info?.userid) {
1650+
// Strange case, when doing WS calls the site should always have the userid already.
1651+
// Apply the patch to match the behaviour of unauthenticated site.
1652+
return true;
1653+
}
1654+
1655+
return Number(patch.userid) === info.userid;
1656+
}
1657+
1658+
/**
1659+
* Get the list of applicable WS overrides for this site.
1660+
*
1661+
* @returns WS overrides that should be applied for this site.
1662+
*/
1663+
getApplicableWSOverrides(): Record<string, CoreWSOverride[]> {
1664+
if (!CoreConstants.CONFIG.wsOverrides) {
1665+
return {};
1666+
}
1667+
1668+
const effectiveOverrides: Record<string, CoreWSOverride[]> = {};
1669+
1670+
Object.keys(CoreConstants.CONFIG.wsOverrides).forEach((method) => {
1671+
const appliedPatches = CoreConstants.CONFIG.wsOverrides![method].filter((patch) =>
1672+
this.shouldApplyWSOverride(method, {}, patch));
1673+
1674+
if (appliedPatches.length) {
1675+
effectiveOverrides[method] = appliedPatches;
1676+
}
1677+
});
1678+
1679+
return effectiveOverrides;
1680+
}
1681+
16301682
}
16311683

16321684
/**

src/core/classes/sites/unauthenticated-site.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ import { CoreText } from '@singletons/text';
2020
import { CoreUrl, CoreUrlPartNames } from '@singletons/url';
2121
import { CoreWS, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws';
2222
import { CorePath } from '@singletons/path';
23+
import { CoreJsonPatch, JsonPatchOperation } from '@singletons/json-patch';
24+
import { CoreUtils } from '@singletons/utils';
25+
import { CoreLogger } from '@singletons/logger';
2326

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

32+
protected logger = CoreLogger.getInstance('CoreUnauthenticatedSite');
33+
2934
siteUrl: string;
3035

3136
protected publicConfig?: CoreSitePublicConfigResponse;
@@ -254,7 +259,7 @@ export class CoreUnauthenticatedSite {
254259
const ignoreCache = options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK ||
255260
options.readingStrategy === CoreSitesReadingStrategy.PREFER_NETWORK;
256261
if (!ignoreCache && this.publicConfig) {
257-
return this.publicConfig;
262+
return this.overridePublicConfig(this.publicConfig);
258263
}
259264

260265
if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_CACHE) {
@@ -266,7 +271,7 @@ export class CoreUnauthenticatedSite {
266271

267272
this.setPublicConfig(config);
268273

269-
return config;
274+
return this.overridePublicConfig(config);
270275
} catch (error) {
271276
if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK || !this.publicConfig) {
272277
throw error;
@@ -298,6 +303,20 @@ export class CoreUnauthenticatedSite {
298303
return {};
299304
}
300305

306+
/**
307+
* Apply overrides to the public config of the site.
308+
*
309+
* @param config Public config.
310+
* @returns Public config with overrides if any.
311+
*/
312+
protected overridePublicConfig(config: CoreSitePublicConfigResponse): CoreSitePublicConfigResponse {
313+
// Always clone the object because it can be modified when applying patches or in the caller function
314+
// and we don't want to modify the stored public config.
315+
const clonedData = CoreUtils.clone(config);
316+
317+
return this.applyWSOverrides('tool_mobile_get_public_config', clonedData);
318+
}
319+
301320
/**
302321
* Perform a request to the server to get the public config of this site.
303322
*
@@ -437,7 +456,7 @@ export class CoreUnauthenticatedSite {
437456
*
438457
* @returns Disabled features.
439458
*/
440-
protected getDisabledFeatures(): string {
459+
getDisabledFeatures(): string {
441460
const siteDisabledFeatures = this.getSiteDisabledFeatures() || undefined; // If empty string, use undefined.
442461
const appDisabledFeatures = CoreConstants.CONFIG.disabledFeatures;
443462

@@ -486,6 +505,69 @@ export class CoreUnauthenticatedSite {
486505
return CoreText.addStartingSlash(CoreUrl.toRelativeURL(this.getURL(), url));
487506
}
488507

508+
/**
509+
* Call a Moodle WS using the AJAX API and applies WebService overrides (if any) to the result.
510+
*
511+
* @param method WS method name.
512+
* @param data Arguments to pass to the method.
513+
* @param preSets Extra settings and information.
514+
* @returns Promise resolved with the response data in success and rejected with CoreAjaxError.
515+
*/
516+
async callAjax<T = unknown>(
517+
method: string,
518+
data: Record<string, unknown> = {},
519+
preSets: Omit<CoreWSAjaxPreSets, 'siteUrl'> = {},
520+
): Promise<T> {
521+
const result = await CoreWS.callAjax<T>(method, data, { ...preSets, siteUrl: this.siteUrl });
522+
523+
// No need to clone the data in this case because it's not stored in any cache.
524+
return this.applyWSOverrides(method, result);
525+
}
526+
527+
/**
528+
* Apply WS overrides (if any) to the data of a WebService response.
529+
*
530+
* @param method WS method name.
531+
* @param data WS response data.
532+
* @returns Modified data (or original data if no overrides).
533+
*/
534+
protected applyWSOverrides<T>(method: string, data: T): T {
535+
if (!CoreConstants.CONFIG.wsOverrides || !CoreConstants.CONFIG.wsOverrides[method]) {
536+
return data;
537+
}
538+
539+
CoreConstants.CONFIG.wsOverrides[method].forEach((patch) => {
540+
if (!this.shouldApplyWSOverride(method, data, patch)) {
541+
this.logger.warn('Patch ignored, conditions not fulfilled:', method, patch);
542+
543+
return;
544+
}
545+
546+
try {
547+
CoreJsonPatch.applyPatch(data, patch);
548+
} catch (error) {
549+
this.logger.error('Error applying WS override:', error, patch);
550+
}
551+
});
552+
553+
return data;
554+
}
555+
556+
/**
557+
* Whether a patch should be applied as a WS override.
558+
*
559+
* @param method WS method name.
560+
* @param data Data returned by the WS.
561+
* @param patch Patch to check.
562+
* @returns Whether it should be applied.
563+
*/
564+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
565+
protected shouldApplyWSOverride(method: string, data: unknown, patch: CoreWSOverride): boolean {
566+
// Always apply patches for unauthenticated sites since we don't have user info.
567+
// If the patch for an AJAX WebService contains an userid is probably by mistake.
568+
return true;
569+
}
570+
489571
}
490572

491573
/**
@@ -637,3 +719,10 @@ export enum TypeOfLogin {
637719
BROWSER = 2, // SSO in browser window is required.
638720
EMBEDDED = 3, // SSO in embedded browser is required.
639721
}
722+
723+
/**
724+
* WebService override patch.
725+
*/
726+
export type CoreWSOverride = JsonPatchOperation & {
727+
userid?: number; // To apply the patch only if the current user matches this userid.
728+
};

src/core/features/login/pages/email-signup/email-signup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export default class CoreLoginEmailSignupPage implements OnInit {
192192
if (configValid) {
193193
// Check content verification.
194194
if (this.ageDigitalConsentVerification === undefined) {
195-
this.ageDigitalConsentVerification = await CoreLoginSignUp.isAgeVerificationEnabled(this.site.getURL());
195+
this.ageDigitalConsentVerification = await CoreLoginSignUp.isAgeVerificationEnabled(this.site);
196196
}
197197

198198
await this.getSignupSettings();
@@ -210,7 +210,7 @@ export default class CoreLoginEmailSignupPage implements OnInit {
210210
* Get signup settings from server.
211211
*/
212212
protected async getSignupSettings(): Promise<void> {
213-
this.settings = await CoreLoginSignUp.getEmailSignupSettings(this.site.getURL());
213+
this.settings = await CoreLoginSignUp.getEmailSignupSettings(this.site);
214214

215215
if (CoreUserProfileFieldDelegate.hasRequiredUnsupportedField(this.settings.profilefields)) {
216216
this.allRequiredSupported = false;
@@ -330,7 +330,7 @@ export default class CoreLoginEmailSignupPage implements OnInit {
330330
this.signupForm.value,
331331
);
332332

333-
const result = await CoreLoginSignUp.emailSignup(userInfo, this.site.getURL(), {
333+
const result = await CoreLoginSignUp.emailSignup(userInfo, this.site, {
334334
recaptchaResponse, customProfileFields, redirect,
335335
});
336336

@@ -410,7 +410,7 @@ export default class CoreLoginEmailSignupPage implements OnInit {
410410

411411
try {
412412
const age = parseInt(this.ageVerificationForm.value.age, 10);
413-
const isMinor = await CoreLoginSignUp.isMinor(age, this.ageVerificationForm.value.country, this.site.getURL());
413+
const isMinor = await CoreLoginSignUp.isMinor(age, this.ageVerificationForm.value.country, this.site);
414414

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

src/core/features/login/services/login-helper.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { CoreApp, CoreStoreConfig } from '@services/app';
2020
import { CoreConfig } from '@services/config';
2121
import { CoreEvents, CoreEventSessionExpiredData, CoreEventSiteData } from '@singletons/events';
2222
import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites';
23-
import { CoreWS, CoreWSExternalWarning } from '@services/ws';
23+
import { CoreWSExternalWarning } from '@services/ws';
2424
import { CoreText } from '@singletons/text';
2525
import { CoreObject } from '@singletons/object';
2626
import { CoreConstants } from '@/core/constants';
@@ -71,6 +71,7 @@ import {
7171
AuthEmailSignupSettings,
7272
CoreLoginSignUp,
7373
} from './signup';
74+
import { CoreSitesFactory } from '@services/sites-factory';
7475

7576
/**
7677
* Helper provider that provides some common features regarding authentication.
@@ -256,7 +257,7 @@ export class CoreLoginHelperProvider {
256257
* @deprecated since 5.2. Please use CoreLoginSignUp.getEmailSignupSettings instead.
257258
*/
258259
async getEmailSignupSettings(siteUrl: string): Promise<AuthEmailSignupSettings> {
259-
return CoreLoginSignUp.getEmailSignupSettings(siteUrl);
260+
return CoreLoginSignUp.getEmailSignupSettings(CoreSitesFactory.makeUnauthenticatedSite(siteUrl));
260261
}
261262

262263
/**
@@ -815,7 +816,7 @@ export class CoreLoginHelperProvider {
815816
params.email = email.trim().toLowerCase();
816817
}
817818

818-
return CoreWS.callAjax('core_auth_request_password_reset', params, { siteUrl });
819+
return CoreSitesFactory.makeUnauthenticatedSite(siteUrl).callAjax('core_auth_request_password_reset', params);
819820
}
820821

821822
/**
@@ -1023,13 +1024,11 @@ export class CoreLoginHelperProvider {
10231024
// Call the WS to resend the confirmation email.
10241025
const modal = await CoreLoadings.show('core.sending', true);
10251026
const data = { username: username?.toLowerCase(), password };
1026-
const preSets = { siteUrl };
10271027

10281028
try {
1029-
const result = <ResendConfirmationEmailResult> await CoreWS.callAjax(
1029+
const result = <ResendConfirmationEmailResult> await CoreSitesFactory.makeUnauthenticatedSite(siteUrl).callAjax(
10301030
'core_auth_resend_confirmation_email',
10311031
data,
1032-
preSets,
10331032
);
10341033

10351034
if (!result.status) {
@@ -1062,7 +1061,7 @@ export class CoreLoginHelperProvider {
10621061
// We don't have site info before login, the only way to check if the WS is available is by calling it.
10631062
try {
10641063
// This call will always fail because we aren't sending parameters.
1065-
await CoreWS.callAjax('core_auth_resend_confirmation_email', {}, { siteUrl });
1064+
await CoreSitesFactory.makeUnauthenticatedSite(siteUrl).callAjax('core_auth_resend_confirmation_email');
10661065

10671066
return true; // We should never reach here.
10681067
} catch (error) {

0 commit comments

Comments
 (0)