diff --git a/scripts/langindex.json b/scripts/langindex.json index 7c05d22dab5..79f8273a47c 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2092,6 +2092,7 @@ "core.login.invalidsite": "local_moodlemobileapp", "core.login.invalidtime": "local_moodlemobileapp", "core.login.invalidurl": "scorm", + "core.login.invalidusername": "moodle", "core.login.invalidvaluemax": "local_moodlemobileapp", "core.login.invalidvaluemin": "local_moodlemobileapp", "core.login.login": "moodle", @@ -2155,6 +2156,7 @@ "core.login.supplyinfo": "moodle", "core.login.toggleremove": "local_moodlemobileapp", "core.login.username": "moodle", + "core.login.usernamelowercase": "moodle", "core.login.usernameoremail": "moodle", "core.login.usernamerequired": "local_moodlemobileapp", "core.login.usernotaddederror": "error", diff --git a/src/core/components/input-errors/core-input-errors.html b/src/core/components/input-errors/core-input-errors.html index c4a3a8a07e3..89de8500094 100644 --- a/src/core/components/input-errors/core-input-errors.html +++ b/src/core/components/input-errors/core-input-errors.html @@ -1,13 +1,18 @@
- {{ errorMessages[error] | translate }} - - {{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }} - - - {{ 'core.login.invalidvaluemin' | translate:{$a: control.errors!.min.min} }} - + + {{ errorMessages[error] | translate }} + + {{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }} + + + {{ 'core.login.invalidvaluemin' | translate:{$a: control.errors!.min.min} }} + + + + {{ errorMessage | translate }} +
diff --git a/src/core/components/input-errors/input-errors.ts b/src/core/components/input-errors/input-errors.ts index da973bd0dd0..61cb6b7916d 100644 --- a/src/core/components/input-errors/input-errors.ts +++ b/src/core/components/input-errors/input-errors.ts @@ -41,7 +41,7 @@ import { FormControl } from '@angular/forms'; export class CoreInputErrorsComponent implements OnInit, OnChanges { @Input() control?: FormControl; // Needed to be able to check the validity of the input. - @Input() errorMessages: Record = {}; // Error messages to show. Keys must be the name of the error. + @Input() errorMessages: CoreInputErrorsMessages = {}; // Error messages to show. Keys must be the name of the error. @Input() errorText = ''; // Set other non automatic errors. errorKeys: string[] = []; @@ -124,4 +124,44 @@ export class CoreInputErrorsComponent implements OnInit, OnChanges { } } + /** + * Get error message for pattern error. + * + * @returns Error message, undefined if not found. + */ + getPatternErrorMessage(): string | undefined { + const patternError = this.control?.errors?.pattern; + if (!this.errorMessages?.pattern || !patternError) { + return; + } + + if (typeof this.errorMessages.pattern === 'string') { + return this.errorMessages.pattern; + } + + return this.errorMessages.pattern[patternError.requiredPattern]; + } + } + +/** + * Error messages for each type of error. + * Error messages will be translated in the template, they don't need to be translated already. + */ +export type CoreInputErrorsMessages = { + required?: string; + requiredTrue?: string; + email?: string; + date?: string; + datetime?: string; + datetimelocal?: string; + time?: string; + url?: string; + max?: string; + min?: string; + maxlength?: string; + minlength?: string; + // For pattern errors you can define an error for all patterns (string), or one error per pattern. + // In the latter case, the key of the object is the pattern and the value is the error message identifier. + pattern?: string | Record; +}; diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index 8dde2f6f7a7..2642ca11e87 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -62,6 +62,7 @@ "invalidsite": "The site URL is not valid.", "invalidtime": "Time not valid", "invalidurl": "Invalid URL specified", + "invalidusername": "The username can only contain alphanumeric lowercase characters (letters and numbers), underscore (_), hyphen (-), period (.) or at symbol (@).", "invalidvaluemax": "The maximum value is {{$a}}", "invalidvaluemin": "The minimum value is {{$a}}", "login": "Log in", @@ -125,6 +126,7 @@ "supplyinfo": "More details", "toggleremove": "Edit accounts list", "username": "Username", + "usernamelowercase": "Only lowercase letters allowed", "usernameoremail": "Enter either username or email address", "usernamerequired": "Username required", "usernotaddederror": "User not added - error", diff --git a/src/core/features/login/pages/email-signup/email-signup.ts b/src/core/features/login/pages/email-signup/email-signup.ts index 45cb6f9fca6..186ba464c64 100644 --- a/src/core/features/login/pages/email-signup/email-signup.ts +++ b/src/core/features/login/pages/email-signup/email-signup.ts @@ -35,6 +35,7 @@ import { CorePath } from '@singletons/path'; import { CoreDom } from '@singletons/dom'; import { CoreSitesFactory } from '@services/sites-factory'; import { EMAIL_SIGNUP_FEATURE_NAME } from '@features/login/constants'; +import { CoreInputErrorsMessages } from '@components/input-errors/input-errors'; /** * Page to signup using email. @@ -46,6 +47,10 @@ import { EMAIL_SIGNUP_FEATURE_NAME } from '@features/login/constants'; }) export class CoreLoginEmailSignupPage implements OnInit { + // Accept A-Z in strict chars pattern to be able to differentiate it from the lowercase pattern. + protected static readonly USERNAME_STRICT_CHARS_PATTERN = '^[A-Z-.@_a-z0-9]*$'; + protected static readonly USERNAME_LOWERCASE_PATTERN = '^[^A-Z]*$'; + @ViewChild(CoreRecaptchaComponent) recaptchaComponent?: CoreRecaptchaComponent; @ViewChild('ageForm') ageFormElement?: ElementRef; @ViewChild('signupFormEl') signupFormElement?: ElementRef; @@ -77,12 +82,12 @@ export class CoreLoginEmailSignupPage implements OnInit { supportEmail?: string; // Validation errors. - usernameErrors: Record; - passwordErrors: Record; - emailErrors: Record; - email2Errors: Record; - policyErrors: Record; - namefieldsErrors?: Record>; + usernameErrors: CoreInputErrorsMessages; + passwordErrors: CoreInputErrorsMessages; + emailErrors: CoreInputErrorsMessages; + email2Errors: CoreInputErrorsMessages; + policyErrors: CoreInputErrorsMessages; + namefieldsErrors?: Record; constructor( protected fb: FormBuilder, @@ -98,14 +103,19 @@ export class CoreLoginEmailSignupPage implements OnInit { // Create the signupForm with the basic controls. More controls will be added later. this.signupForm = this.fb.group({ - username: ['', Validators.required], password: ['', Validators.required], email: ['', Validators.compose([Validators.required, Validators.email])], email2: ['', Validators.compose([Validators.required, Validators.email])], }); // Setup validation errors. - this.usernameErrors = { required: 'core.login.usernamerequired' }; + this.usernameErrors = { + required: 'core.login.usernamerequired', + pattern: { + [CoreLoginEmailSignupPage.USERNAME_STRICT_CHARS_PATTERN]: 'core.login.invalidusername', + [CoreLoginEmailSignupPage.USERNAME_LOWERCASE_PATTERN]: 'core.login.usernamelowercase', + }, + }; this.passwordErrors = { required: 'core.login.passwordrequired' }; this.emailErrors = { required: 'core.login.missingemail' }; this.policyErrors = { required: 'core.login.policyagree' }; @@ -140,6 +150,13 @@ export class CoreLoginEmailSignupPage implements OnInit { * Complete the FormGroup using the settings received from server. */ protected completeFormGroup(): void { + const checkStrictChars = this.settings?.extendedusernamechars === false; + this.signupForm.addControl('username', this.fb.control('', Validators.compose([ + Validators.required, + Validators.pattern(CoreLoginEmailSignupPage.USERNAME_LOWERCASE_PATTERN), + checkStrictChars ? Validators.pattern(CoreLoginEmailSignupPage.USERNAME_STRICT_CHARS_PATTERN) : undefined, + ]))); + this.signupForm.addControl('city', this.fb.control(this.settings?.defaultcity || '')); this.signUpCountryControl = this.fb.control(this.settings?.country || '', { nonNullable: true }); this.signupForm.addControl('country', this.signUpCountryControl); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index a8c22c345ac..c80fc521e16 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -1553,6 +1553,7 @@ export type AuthEmailSignupSettings = { sitepolicyhandler?: string; // Site policy handler. defaultcity?: string; // Default city. country?: string; // Default country. + extendedusernamechars?: boolean; // @since 4.4. Extended characters in usernames or no. profilefields?: AuthEmailSignupProfileField[]; // Required profile fields. recaptchapublickey?: string; // Recaptcha public key. recaptchachallengehash?: string; // Recaptcha challenge hash. diff --git a/src/core/features/login/tests/behat/signup.feature b/src/core/features/login/tests/behat/signup.feature index 1a56ac5e0ab..6704798b027 100755 --- a/src/core/features/login/tests/behat/signup.feature +++ b/src/core/features/login/tests/behat/signup.feature @@ -184,3 +184,44 @@ Feature: Test signup in app And I should find "1 January 2010, 11:45 AM" in the app And I should find "This is my description" in the app And I should find "https://moodle.com" in the app + + @lms_from4.4 + Scenario: Check extended characters in usernames show error if setting is disabled + Given the following config values are set as admin: + | extendedusernamechars | 0 | + When I launch the app + And I set the field "Your site" to "$WWWROOT" in the app + And I press "Connect to your site" in the app + And I press "Create new account" in the app + And I set the following fields to these values in the app: + | Username | u1$ | + Then I should find "The username can only contain alphanumeric" in the app + + When I set the following fields to these values in the app: + | Username | u1 | + Then I should not find "The username can only contain alphanumeric" in the app + + @lms_from4.4 + Scenario: Check can include extended characters in usernames if setting is enabled + Given the following config values are set as admin: + | extendedusernamechars | 1 | + When I launch the app + And I set the field "Your site" to "$WWWROOT" in the app + And I press "Connect to your site" in the app + And I press "Create new account" in the app + And I set the following fields to these values in the app: + | Username | u1U | + Then I should find "Only lowercase letters allowed" in the app + + When I set the following fields to these values in the app: + | Username | u1$ | + Then I should not find "The username can only contain alphanumeric" in the app + + When I set the following fields to these values in the app: + | Password | pu1 | + | Email address | u1@u1.com | + | Email (again) | u1@u1.com | + | First name | User | + | Last name | Test | + And I press "Create my new account" in the app + Then I should find "An email should have been sent to your address" in the app