]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
1537c1500045882ec8e588bf771cfe92369f2c1e
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / forms / cd-validators.ts
1 import {
2 AbstractControl,
3 AsyncValidatorFn,
4 ValidationErrors,
5 ValidatorFn,
6 Validators
7 } from '@angular/forms';
8
9 import _ from 'lodash';
10 import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
11 import { map, switchMapTo, take } from 'rxjs/operators';
12
13 import { DimlessBinaryPipe } from '../pipes/dimless-binary.pipe';
14 import { FormatterService } from '../services/formatter.service';
15
16 export function isEmptyInputValue(value: any): boolean {
17 return value == null || value.length === 0;
18 }
19
20 export type existsServiceFn = (value: any) => Observable<boolean>;
21
22 export class CdValidators {
23 /**
24 * Validator that performs email validation. In contrast to the Angular
25 * email validator an empty email will not be handled as invalid.
26 */
27 static email(control: AbstractControl): ValidationErrors | null {
28 // Exit immediately if value is empty.
29 if (isEmptyInputValue(control.value)) {
30 return null;
31 }
32 return Validators.email(control);
33 }
34
35 /**
36 * Validator function in order to validate IP addresses.
37 * @param {number} version determines the protocol version. It needs to be set to 4 for IPv4 and
38 * to 6 for IPv6 validation. For any other number (it's also the default case) it will return a
39 * function to validate the input string against IPv4 OR IPv6.
40 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
41 * if the validation check fails, otherwise `null`.
42 */
43 static ip(version: number = 0): ValidatorFn {
44 // prettier-ignore
45 const ipv4Rgx =
46 /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
47 const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
48
49 if (version === 4) {
50 return Validators.pattern(ipv4Rgx);
51 } else if (version === 6) {
52 return Validators.pattern(ipv6Rgx);
53 } else {
54 return Validators.pattern(new RegExp(ipv4Rgx.source + '|' + ipv6Rgx.source));
55 }
56 }
57
58 /**
59 * Validator function in order to validate numbers.
60 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
61 * if the validation check fails, otherwise `null`.
62 */
63 static number(allowsNegative: boolean = true): ValidatorFn {
64 if (allowsNegative) {
65 return Validators.pattern(/^-?[0-9]+$/i);
66 } else {
67 return Validators.pattern(/^[0-9]+$/i);
68 }
69 }
70
71 /**
72 * Validator function in order to validate decimal numbers.
73 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
74 * if the validation check fails, otherwise `null`.
75 */
76 static decimalNumber(allowsNegative: boolean = true): ValidatorFn {
77 if (allowsNegative) {
78 return Validators.pattern(/^-?[0-9]+(.[0-9]+)?$/i);
79 } else {
80 return Validators.pattern(/^[0-9]+(.[0-9]+)?$/i);
81 }
82 }
83
84 /**
85 * Validator that performs SSL certificate validation.
86 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
87 * if the validation check fails, otherwise `null`.
88 */
89 static sslCert(): ValidatorFn {
90 return Validators.pattern(
91 /^-----BEGIN CERTIFICATE-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END CERTIFICATE-----[\n\r\f]*$/
92 );
93 }
94
95 /**
96 * Validator that performs SSL private key validation.
97 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
98 * if the validation check fails, otherwise `null`.
99 */
100 static sslPrivKey(): ValidatorFn {
101 return Validators.pattern(
102 /^-----BEGIN RSA PRIVATE KEY-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END RSA PRIVATE KEY-----[\n\r\f]*$/
103 );
104 }
105
106 /**
107 * Validator that requires controls to fulfill the specified condition if
108 * the specified prerequisites matches. If the prerequisites are fulfilled,
109 * then the given function is executed and if it succeeds, the 'required'
110 * validation error will be returned, otherwise null.
111 * @param {Object} prerequisites An object containing the prerequisites.
112 * To do additional checks rather than checking for equality you can
113 * use the extended prerequisite syntax:
114 * 'field_name': { 'op': '<OPERATOR>', arg1: '<OPERATOR_ARGUMENT>' }
115 * The following operators are supported:
116 * * empty
117 * * !empty
118 * * equal
119 * * !equal
120 * * minLength
121 * ### Example
122 * ```typescript
123 * {
124 * 'generate_key': true,
125 * 'username': 'Max Mustermann'
126 * }
127 * ```
128 * ### Example - Extended prerequisites
129 * ```typescript
130 * {
131 * 'generate_key': { 'op': 'equal', 'arg1': true },
132 * 'username': { 'op': 'minLength', 'arg1': 5 }
133 * }
134 * ```
135 * Only if all prerequisites are fulfilled, then the validation of the
136 * control will be triggered.
137 * @param {Function | undefined} condition The function to be executed when all
138 * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue}
139 * function will be used by default. The control's value is used as function
140 * argument. The function must return true to set the validation error.
141 * @return {ValidatorFn} Returns the validator function.
142 */
143 static requiredIf(prerequisites: object, condition?: Function | undefined): ValidatorFn {
144 let isWatched = false;
145
146 return (control: AbstractControl): ValidationErrors | null => {
147 if (!isWatched && control.parent) {
148 Object.keys(prerequisites).forEach((key) => {
149 control.parent.get(key).valueChanges.subscribe(() => {
150 control.updateValueAndValidity({ emitEvent: false });
151 });
152 });
153
154 isWatched = true;
155 }
156
157 // Check if all prerequisites met.
158 if (
159 !Object.keys(prerequisites).every((key) => {
160 if (!control.parent) {
161 return false;
162 }
163 const value = control.parent.get(key).value;
164 const prerequisite = prerequisites[key];
165 if (_.isObjectLike(prerequisite)) {
166 let result = false;
167 switch (prerequisite['op']) {
168 case 'empty':
169 result = _.isEmpty(value);
170 break;
171 case '!empty':
172 result = !_.isEmpty(value);
173 break;
174 case 'equal':
175 result = value === prerequisite['arg1'];
176 break;
177 case '!equal':
178 result = value !== prerequisite['arg1'];
179 break;
180 case 'minLength':
181 if (_.isString(value)) {
182 result = value.length >= prerequisite['arg1'];
183 }
184 break;
185 }
186 return result;
187 }
188 return value === prerequisite;
189 })
190 ) {
191 return null;
192 }
193 const success = _.isFunction(condition)
194 ? condition.call(condition, control.value)
195 : isEmptyInputValue(control.value);
196 return success ? { required: true } : null;
197 };
198 }
199
200 /**
201 * Compose multiple validators into a single function that returns the union of
202 * the individual error maps for the provided control when the given prerequisites
203 * are fulfilled.
204 *
205 * @param {Object} prerequisites An object containing the prerequisites as
206 * key/value pairs.
207 * ### Example
208 * ```typescript
209 * {
210 * 'generate_key': true,
211 * 'username': 'Max Mustermann'
212 * }
213 * ```
214 * @param {ValidatorFn[]} validators List of validators that should be taken
215 * into action when the prerequisites are met.
216 * @return {ValidatorFn} Returns the validator function.
217 */
218 static composeIf(prerequisites: object, validators: ValidatorFn[]): ValidatorFn {
219 let isWatched = false;
220 return (control: AbstractControl): ValidationErrors | null => {
221 if (!isWatched && control.parent) {
222 Object.keys(prerequisites).forEach((key) => {
223 control.parent.get(key).valueChanges.subscribe(() => {
224 control.updateValueAndValidity({ emitEvent: false });
225 });
226 });
227 isWatched = true;
228 }
229 // Check if all prerequisites are met.
230 if (
231 !Object.keys(prerequisites).every((key) => {
232 return control.parent && control.parent.get(key).value === prerequisites[key];
233 })
234 ) {
235 return null;
236 }
237 return Validators.compose(validators)(control);
238 };
239 }
240
241 /**
242 * Custom validation by passing a name for the error and a function as error condition.
243 *
244 * @param {string} error
245 * @param {Function} condition - a truthy return value will trigger the error
246 * @returns {ValidatorFn}
247 */
248 static custom(error: string, condition: Function): ValidatorFn {
249 return (control: AbstractControl): { [key: string]: any } => {
250 const value = condition.call(this, control.value);
251 if (value) {
252 return { [error]: value };
253 }
254 return null;
255 };
256 }
257
258 /**
259 * Validate form control if condition is true with validators.
260 *
261 * @param {AbstractControl} formControl
262 * @param {Function} condition
263 * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested
264 * when the condition is met
265 * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested
266 * @param {AbstractControl[]} watchControls List of controls that the condition depend on.
267 * Every time one of this controls value is updated, the validation will be triggered
268 */
269 static validateIf(
270 formControl: AbstractControl,
271 condition: Function,
272 conditionalValidators: ValidatorFn[],
273 permanentValidators: ValidatorFn[] = [],
274 watchControls: AbstractControl[] = []
275 ) {
276 conditionalValidators = conditionalValidators.concat(permanentValidators);
277
278 formControl.setValidators((control: AbstractControl): {
279 [key: string]: any;
280 } => {
281 const value = condition.call(this);
282 if (value) {
283 return Validators.compose(conditionalValidators)(control);
284 }
285 if (permanentValidators.length > 0) {
286 return Validators.compose(permanentValidators)(control);
287 }
288 return null;
289 });
290
291 watchControls.forEach((control: AbstractControl) => {
292 control.valueChanges.subscribe(() => {
293 formControl.updateValueAndValidity({ emitEvent: false });
294 });
295 });
296 }
297
298 /**
299 * Validator that requires that both specified controls have the same value.
300 * Error will be added to the `path2` control.
301 * @param {string} path1 A dot-delimited string that define the path to the control.
302 * @param {string} path2 A dot-delimited string that define the path to the control.
303 * @return {ValidatorFn} Returns a validator function that always returns `null`.
304 * If the validation fails an error map with the `match` property will be set
305 * on the `path2` control.
306 */
307 static match(path1: string, path2: string): ValidatorFn {
308 return (control: AbstractControl): { [key: string]: any } => {
309 const ctrl1 = control.get(path1);
310 const ctrl2 = control.get(path2);
311 if (!ctrl1 || !ctrl2) {
312 return null;
313 }
314 if (ctrl1.value !== ctrl2.value) {
315 ctrl2.setErrors({ match: true });
316 } else {
317 const hasError = ctrl2.hasError('match');
318 if (hasError) {
319 // Remove the 'match' error. If no more errors exists, then set
320 // the error value to 'null', otherwise the field is still marked
321 // as invalid.
322 const errors = ctrl2.errors;
323 _.unset(errors, 'match');
324 ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors);
325 }
326 }
327 return null;
328 };
329 }
330
331 /**
332 * Asynchronous validator that requires the control's value to be unique.
333 * The validation is only executed after the specified delay. Every
334 * keystroke during this delay will restart the timer.
335 * @param serviceFn {existsServiceFn} The service function that is
336 * called to check whether the given value exists. It must return
337 * boolean 'true' if the given value exists, otherwise 'false'.
338 * @param serviceFnThis {any} The object to be used as the 'this' object
339 * when calling the serviceFn function. Defaults to null.
340 * @param {number|Date} dueTime The delay time to wait before the
341 * serviceFn call is executed. This is useful to prevent calls on
342 * every keystroke. Defaults to 500.
343 * @return {AsyncValidatorFn} Returns an asynchronous validator function
344 * that returns an error map with the `notUnique` property if the
345 * validation check succeeds, otherwise `null`.
346 */
347 static unique(
348 serviceFn: existsServiceFn,
349 serviceFnThis: any = null,
350 usernameFn?: Function,
351 uidfield = false,
352 dueTime = 500
353 ): AsyncValidatorFn {
354 let uname: string;
355 return (control: AbstractControl): Observable<ValidationErrors | null> => {
356 // Exit immediately if user has not interacted with the control yet
357 // or the control value is empty.
358 if (control.pristine || isEmptyInputValue(control.value)) {
359 return observableOf(null);
360 }
361 uname = control.value;
362 if (_.isFunction(usernameFn) && usernameFn() !== null && usernameFn() !== '') {
363 if (uidfield) {
364 uname = `${control.value}$${usernameFn()}`;
365 } else {
366 uname = `${usernameFn()}$${control.value}`;
367 }
368 }
369
370 // Forgot previous requests if a new one arrives within the specified
371 // delay time.
372 return observableTimer(dueTime).pipe(
373 switchMapTo(serviceFn.call(serviceFnThis, uname)),
374 map((resp: boolean) => {
375 if (!resp) {
376 return null;
377 } else {
378 return { notUnique: true };
379 }
380 }),
381 take(1)
382 );
383 };
384 }
385
386 /**
387 * Validator function for UUIDs.
388 * @param required - Defines if it is mandatory to fill in the UUID
389 * @return Validator function that returns an error object containing `invalidUuid` if the
390 * validation failed, `null` otherwise.
391 */
392 static uuid(required = false): ValidatorFn {
393 const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
394 return (control: AbstractControl): { [key: string]: any } | null => {
395 if (control.pristine && control.untouched) {
396 return null;
397 } else if (!required && !control.value) {
398 return null;
399 } else if (uuidRe.test(control.value)) {
400 return null;
401 }
402 return { invalidUuid: 'This is not a valid UUID' };
403 };
404 }
405
406 /**
407 * A simple minimum validator vor cd-binary inputs.
408 *
409 * To use the validation message pass I18n into the function as it cannot
410 * be called in a static one.
411 */
412 static binaryMin(bytes: number): ValidatorFn {
413 return (control: AbstractControl): { [key: string]: () => string } | null => {
414 const formatterService = new FormatterService();
415 const currentBytes = new FormatterService().toBytes(control.value);
416 if (bytes <= currentBytes) {
417 return null;
418 }
419 const value = new DimlessBinaryPipe(formatterService).transform(bytes);
420 return {
421 binaryMin: () => $localize`Size has to be at least ${value} or more`
422 };
423 };
424 }
425
426 /**
427 * A simple maximum validator vor cd-binary inputs.
428 *
429 * To use the validation message pass I18n into the function as it cannot
430 * be called in a static one.
431 */
432 static binaryMax(bytes: number): ValidatorFn {
433 return (control: AbstractControl): { [key: string]: () => string } | null => {
434 const formatterService = new FormatterService();
435 const currentBytes = formatterService.toBytes(control.value);
436 if (bytes >= currentBytes) {
437 return null;
438 }
439 const value = new DimlessBinaryPipe(formatterService).transform(bytes);
440 return {
441 binaryMax: () => $localize`Size has to be at most ${value} or less`
442 };
443 };
444 }
445
446 /**
447 * Asynchronous validator that checks if the password meets the password
448 * policy.
449 * @param userServiceThis The object to be used as the 'this' object
450 * when calling the 'validatePassword' method of the 'UserService'.
451 * @param usernameFn Function to get the username that should be
452 * taken into account.
453 * @param callback Callback function that is called after the validation
454 * has been done.
455 * @return {AsyncValidatorFn} Returns an asynchronous validator function
456 * that returns an error map with the `passwordPolicy` property if the
457 * validation check fails, otherwise `null`.
458 */
459 static passwordPolicy(
460 userServiceThis: any,
461 usernameFn?: Function,
462 callback?: (valid: boolean, credits?: number, valuation?: string) => void
463 ): AsyncValidatorFn {
464 return (control: AbstractControl): Observable<ValidationErrors | null> => {
465 if (control.pristine || control.value === '') {
466 if (_.isFunction(callback)) {
467 callback(true, 0);
468 }
469 return observableOf(null);
470 }
471 let username;
472 if (_.isFunction(usernameFn)) {
473 username = usernameFn();
474 }
475 return observableTimer(500).pipe(
476 switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)),
477 map((resp: { valid: boolean; credits: number; valuation: string }) => {
478 if (_.isFunction(callback)) {
479 callback(resp.valid, resp.credits, resp.valuation);
480 }
481 if (resp.valid) {
482 return null;
483 } else {
484 return { passwordPolicy: true };
485 }
486 }),
487 take(1)
488 );
489 };
490 }
491 }