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