]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | import { |
2 | AbstractControl, | |
3 | AsyncValidatorFn, | |
4 | ValidationErrors, | |
5 | ValidatorFn, | |
6 | Validators | |
7 | } from '@angular/forms'; | |
8 | ||
f67539c2 | 9 | import _ from 'lodash'; |
11fdf7f2 TL |
10 | import { Observable, of as observableOf, timer as observableTimer } from 'rxjs'; |
11 | import { map, switchMapTo, take } from 'rxjs/operators'; | |
12 | ||
a4b75251 TL |
13 | import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; |
14 | import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; | |
15 | import { FormatterService } from '~/app/shared/services/formatter.service'; | |
9f95a23c | 16 | |
11fdf7f2 TL |
17 | export function isEmptyInputValue(value: any): boolean { |
18 | return value == null || value.length === 0; | |
19 | } | |
20 | ||
aee94f69 | 21 | export type existsServiceFn = (value: any, args?: any) => Observable<boolean>; |
11fdf7f2 TL |
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 | |
adb31ebb TL |
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. | |
11fdf7f2 | 41 | * @returns {ValidatorFn} A validator function that returns an error map containing `pattern` |
adb31ebb | 42 | * if the validation check fails, otherwise `null`. |
11fdf7f2 TL |
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` | |
adb31ebb | 62 | * if the validation check fails, otherwise `null`. |
11fdf7f2 TL |
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` | |
adb31ebb | 75 | * if the validation check fails, otherwise `null`. |
11fdf7f2 TL |
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 | ||
adb31ebb TL |
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 | ||
522d829b TL |
107 | /** |
108 | * Validator that performs SSL certificate validation of pem format. | |
109 | * @returns {ValidatorFn} A validator function that returns an error map containing `pattern` | |
110 | * if the validation check fails, otherwise `null`. | |
111 | */ | |
112 | static pemCert(): ValidatorFn { | |
113 | return Validators.pattern(/^-----BEGIN .+-----$.+^-----END .+-----$/ms); | |
114 | } | |
115 | ||
11fdf7f2 TL |
116 | /** |
117 | * Validator that requires controls to fulfill the specified condition if | |
118 | * the specified prerequisites matches. If the prerequisites are fulfilled, | |
119 | * then the given function is executed and if it succeeds, the 'required' | |
120 | * validation error will be returned, otherwise null. | |
121 | * @param {Object} prerequisites An object containing the prerequisites. | |
adb31ebb TL |
122 | * To do additional checks rather than checking for equality you can |
123 | * use the extended prerequisite syntax: | |
124 | * 'field_name': { 'op': '<OPERATOR>', arg1: '<OPERATOR_ARGUMENT>' } | |
125 | * The following operators are supported: | |
126 | * * empty | |
127 | * * !empty | |
128 | * * equal | |
129 | * * !equal | |
130 | * * minLength | |
11fdf7f2 TL |
131 | * ### Example |
132 | * ```typescript | |
133 | * { | |
134 | * 'generate_key': true, | |
135 | * 'username': 'Max Mustermann' | |
136 | * } | |
137 | * ``` | |
adb31ebb TL |
138 | * ### Example - Extended prerequisites |
139 | * ```typescript | |
140 | * { | |
141 | * 'generate_key': { 'op': 'equal', 'arg1': true }, | |
142 | * 'username': { 'op': 'minLength', 'arg1': 5 } | |
143 | * } | |
144 | * ``` | |
11fdf7f2 TL |
145 | * Only if all prerequisites are fulfilled, then the validation of the |
146 | * control will be triggered. | |
147 | * @param {Function | undefined} condition The function to be executed when all | |
148 | * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue} | |
149 | * function will be used by default. The control's value is used as function | |
150 | * argument. The function must return true to set the validation error. | |
151 | * @return {ValidatorFn} Returns the validator function. | |
152 | */ | |
9f95a23c | 153 | static requiredIf(prerequisites: object, condition?: Function | undefined): ValidatorFn { |
81eedcae TL |
154 | let isWatched = false; |
155 | ||
11fdf7f2 | 156 | return (control: AbstractControl): ValidationErrors | null => { |
81eedcae TL |
157 | if (!isWatched && control.parent) { |
158 | Object.keys(prerequisites).forEach((key) => { | |
159 | control.parent.get(key).valueChanges.subscribe(() => { | |
160 | control.updateValueAndValidity({ emitEvent: false }); | |
161 | }); | |
162 | }); | |
163 | ||
164 | isWatched = true; | |
165 | } | |
166 | ||
9f95a23c | 167 | // Check if all prerequisites met. |
11fdf7f2 TL |
168 | if ( |
169 | !Object.keys(prerequisites).every((key) => { | |
adb31ebb TL |
170 | if (!control.parent) { |
171 | return false; | |
172 | } | |
173 | const value = control.parent.get(key).value; | |
174 | const prerequisite = prerequisites[key]; | |
175 | if (_.isObjectLike(prerequisite)) { | |
176 | let result = false; | |
177 | switch (prerequisite['op']) { | |
178 | case 'empty': | |
179 | result = _.isEmpty(value); | |
180 | break; | |
181 | case '!empty': | |
182 | result = !_.isEmpty(value); | |
183 | break; | |
184 | case 'equal': | |
185 | result = value === prerequisite['arg1']; | |
186 | break; | |
187 | case '!equal': | |
188 | result = value !== prerequisite['arg1']; | |
189 | break; | |
190 | case 'minLength': | |
191 | if (_.isString(value)) { | |
192 | result = value.length >= prerequisite['arg1']; | |
193 | } | |
194 | break; | |
195 | } | |
196 | return result; | |
197 | } | |
198 | return value === prerequisite; | |
11fdf7f2 TL |
199 | }) |
200 | ) { | |
201 | return null; | |
202 | } | |
203 | const success = _.isFunction(condition) | |
204 | ? condition.call(condition, control.value) | |
205 | : isEmptyInputValue(control.value); | |
206 | return success ? { required: true } : null; | |
207 | }; | |
208 | } | |
209 | ||
9f95a23c TL |
210 | /** |
211 | * Compose multiple validators into a single function that returns the union of | |
212 | * the individual error maps for the provided control when the given prerequisites | |
213 | * are fulfilled. | |
214 | * | |
215 | * @param {Object} prerequisites An object containing the prerequisites as | |
216 | * key/value pairs. | |
217 | * ### Example | |
218 | * ```typescript | |
219 | * { | |
220 | * 'generate_key': true, | |
221 | * 'username': 'Max Mustermann' | |
222 | * } | |
223 | * ``` | |
224 | * @param {ValidatorFn[]} validators List of validators that should be taken | |
225 | * into action when the prerequisites are met. | |
226 | * @return {ValidatorFn} Returns the validator function. | |
227 | */ | |
228 | static composeIf(prerequisites: object, validators: ValidatorFn[]): ValidatorFn { | |
229 | let isWatched = false; | |
230 | return (control: AbstractControl): ValidationErrors | null => { | |
231 | if (!isWatched && control.parent) { | |
232 | Object.keys(prerequisites).forEach((key) => { | |
233 | control.parent.get(key).valueChanges.subscribe(() => { | |
234 | control.updateValueAndValidity({ emitEvent: false }); | |
235 | }); | |
236 | }); | |
237 | isWatched = true; | |
238 | } | |
239 | // Check if all prerequisites are met. | |
240 | if ( | |
241 | !Object.keys(prerequisites).every((key) => { | |
242 | return control.parent && control.parent.get(key).value === prerequisites[key]; | |
243 | }) | |
244 | ) { | |
245 | return null; | |
246 | } | |
247 | return Validators.compose(validators)(control); | |
248 | }; | |
249 | } | |
250 | ||
11fdf7f2 TL |
251 | /** |
252 | * Custom validation by passing a name for the error and a function as error condition. | |
253 | * | |
254 | * @param {string} error | |
255 | * @param {Function} condition - a truthy return value will trigger the error | |
256 | * @returns {ValidatorFn} | |
257 | */ | |
258 | static custom(error: string, condition: Function): ValidatorFn { | |
259 | return (control: AbstractControl): { [key: string]: any } => { | |
260 | const value = condition.call(this, control.value); | |
261 | if (value) { | |
262 | return { [error]: value }; | |
263 | } | |
264 | return null; | |
265 | }; | |
266 | } | |
267 | ||
268 | /** | |
269 | * Validate form control if condition is true with validators. | |
270 | * | |
271 | * @param {AbstractControl} formControl | |
272 | * @param {Function} condition | |
273 | * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested | |
274 | * when the condition is met | |
275 | * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested | |
276 | * @param {AbstractControl[]} watchControls List of controls that the condition depend on. | |
277 | * Every time one of this controls value is updated, the validation will be triggered | |
278 | */ | |
279 | static validateIf( | |
280 | formControl: AbstractControl, | |
281 | condition: Function, | |
282 | conditionalValidators: ValidatorFn[], | |
283 | permanentValidators: ValidatorFn[] = [], | |
284 | watchControls: AbstractControl[] = [] | |
285 | ) { | |
286 | conditionalValidators = conditionalValidators.concat(permanentValidators); | |
287 | ||
9f95a23c TL |
288 | formControl.setValidators((control: AbstractControl): { |
289 | [key: string]: any; | |
290 | } => { | |
291 | const value = condition.call(this); | |
292 | if (value) { | |
293 | return Validators.compose(conditionalValidators)(control); | |
11fdf7f2 | 294 | } |
9f95a23c TL |
295 | if (permanentValidators.length > 0) { |
296 | return Validators.compose(permanentValidators)(control); | |
297 | } | |
298 | return null; | |
299 | }); | |
11fdf7f2 TL |
300 | |
301 | watchControls.forEach((control: AbstractControl) => { | |
302 | control.valueChanges.subscribe(() => { | |
303 | formControl.updateValueAndValidity({ emitEvent: false }); | |
304 | }); | |
305 | }); | |
306 | } | |
307 | ||
308 | /** | |
309 | * Validator that requires that both specified controls have the same value. | |
310 | * Error will be added to the `path2` control. | |
311 | * @param {string} path1 A dot-delimited string that define the path to the control. | |
312 | * @param {string} path2 A dot-delimited string that define the path to the control. | |
313 | * @return {ValidatorFn} Returns a validator function that always returns `null`. | |
314 | * If the validation fails an error map with the `match` property will be set | |
315 | * on the `path2` control. | |
316 | */ | |
317 | static match(path1: string, path2: string): ValidatorFn { | |
318 | return (control: AbstractControl): { [key: string]: any } => { | |
319 | const ctrl1 = control.get(path1); | |
320 | const ctrl2 = control.get(path2); | |
9f95a23c TL |
321 | if (!ctrl1 || !ctrl2) { |
322 | return null; | |
323 | } | |
11fdf7f2 TL |
324 | if (ctrl1.value !== ctrl2.value) { |
325 | ctrl2.setErrors({ match: true }); | |
326 | } else { | |
327 | const hasError = ctrl2.hasError('match'); | |
328 | if (hasError) { | |
329 | // Remove the 'match' error. If no more errors exists, then set | |
330 | // the error value to 'null', otherwise the field is still marked | |
331 | // as invalid. | |
332 | const errors = ctrl2.errors; | |
333 | _.unset(errors, 'match'); | |
334 | ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors); | |
335 | } | |
336 | } | |
337 | return null; | |
338 | }; | |
339 | } | |
340 | ||
341 | /** | |
342 | * Asynchronous validator that requires the control's value to be unique. | |
343 | * The validation is only executed after the specified delay. Every | |
344 | * keystroke during this delay will restart the timer. | |
345 | * @param serviceFn {existsServiceFn} The service function that is | |
346 | * called to check whether the given value exists. It must return | |
347 | * boolean 'true' if the given value exists, otherwise 'false'. | |
348 | * @param serviceFnThis {any} The object to be used as the 'this' object | |
349 | * when calling the serviceFn function. Defaults to null. | |
350 | * @param {number|Date} dueTime The delay time to wait before the | |
351 | * serviceFn call is executed. This is useful to prevent calls on | |
352 | * every keystroke. Defaults to 500. | |
353 | * @return {AsyncValidatorFn} Returns an asynchronous validator function | |
354 | * that returns an error map with the `notUnique` property if the | |
355 | * validation check succeeds, otherwise `null`. | |
356 | */ | |
357 | static unique( | |
358 | serviceFn: existsServiceFn, | |
359 | serviceFnThis: any = null, | |
f67539c2 | 360 | usernameFn?: Function, |
aee94f69 TL |
361 | uidField = false, |
362 | extraArgs = '' | |
11fdf7f2 | 363 | ): AsyncValidatorFn { |
b3b6e05e | 364 | let uName: string; |
11fdf7f2 TL |
365 | return (control: AbstractControl): Observable<ValidationErrors | null> => { |
366 | // Exit immediately if user has not interacted with the control yet | |
367 | // or the control value is empty. | |
368 | if (control.pristine || isEmptyInputValue(control.value)) { | |
369 | return observableOf(null); | |
370 | } | |
b3b6e05e | 371 | uName = control.value; |
f67539c2 | 372 | if (_.isFunction(usernameFn) && usernameFn() !== null && usernameFn() !== '') { |
b3b6e05e TL |
373 | if (uidField) { |
374 | uName = `${control.value}$${usernameFn()}`; | |
f67539c2 | 375 | } else { |
b3b6e05e | 376 | uName = `${usernameFn()}$${control.value}`; |
f67539c2 TL |
377 | } |
378 | } | |
379 | ||
b3b6e05e | 380 | return observableTimer().pipe( |
aee94f69 | 381 | switchMapTo(serviceFn.call(serviceFnThis, uName, extraArgs)), |
11fdf7f2 TL |
382 | map((resp: boolean) => { |
383 | if (!resp) { | |
384 | return null; | |
385 | } else { | |
386 | return { notUnique: true }; | |
387 | } | |
388 | }), | |
389 | take(1) | |
390 | ); | |
391 | }; | |
392 | } | |
393 | ||
394 | /** | |
395 | * Validator function for UUIDs. | |
396 | * @param required - Defines if it is mandatory to fill in the UUID | |
397 | * @return Validator function that returns an error object containing `invalidUuid` if the | |
398 | * validation failed, `null` otherwise. | |
399 | */ | |
400 | static uuid(required = false): ValidatorFn { | |
401 | 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; | |
402 | return (control: AbstractControl): { [key: string]: any } | null => { | |
403 | if (control.pristine && control.untouched) { | |
404 | return null; | |
405 | } else if (!required && !control.value) { | |
406 | return null; | |
407 | } else if (uuidRe.test(control.value)) { | |
408 | return null; | |
409 | } | |
410 | return { invalidUuid: 'This is not a valid UUID' }; | |
411 | }; | |
412 | } | |
9f95a23c TL |
413 | |
414 | /** | |
415 | * A simple minimum validator vor cd-binary inputs. | |
416 | * | |
417 | * To use the validation message pass I18n into the function as it cannot | |
418 | * be called in a static one. | |
419 | */ | |
420 | static binaryMin(bytes: number): ValidatorFn { | |
f67539c2 | 421 | return (control: AbstractControl): { [key: string]: () => string } | null => { |
9f95a23c TL |
422 | const formatterService = new FormatterService(); |
423 | const currentBytes = new FormatterService().toBytes(control.value); | |
424 | if (bytes <= currentBytes) { | |
425 | return null; | |
426 | } | |
427 | const value = new DimlessBinaryPipe(formatterService).transform(bytes); | |
428 | return { | |
f67539c2 | 429 | binaryMin: () => $localize`Size has to be at least ${value} or more` |
9f95a23c TL |
430 | }; |
431 | }; | |
432 | } | |
433 | ||
434 | /** | |
435 | * A simple maximum validator vor cd-binary inputs. | |
436 | * | |
437 | * To use the validation message pass I18n into the function as it cannot | |
438 | * be called in a static one. | |
439 | */ | |
440 | static binaryMax(bytes: number): ValidatorFn { | |
f67539c2 | 441 | return (control: AbstractControl): { [key: string]: () => string } | null => { |
9f95a23c TL |
442 | const formatterService = new FormatterService(); |
443 | const currentBytes = formatterService.toBytes(control.value); | |
444 | if (bytes >= currentBytes) { | |
445 | return null; | |
446 | } | |
447 | const value = new DimlessBinaryPipe(formatterService).transform(bytes); | |
448 | return { | |
f67539c2 | 449 | binaryMax: () => $localize`Size has to be at most ${value} or less` |
9f95a23c TL |
450 | }; |
451 | }; | |
452 | } | |
453 | ||
454 | /** | |
455 | * Asynchronous validator that checks if the password meets the password | |
456 | * policy. | |
457 | * @param userServiceThis The object to be used as the 'this' object | |
458 | * when calling the 'validatePassword' method of the 'UserService'. | |
459 | * @param usernameFn Function to get the username that should be | |
460 | * taken into account. | |
461 | * @param callback Callback function that is called after the validation | |
462 | * has been done. | |
463 | * @return {AsyncValidatorFn} Returns an asynchronous validator function | |
464 | * that returns an error map with the `passwordPolicy` property if the | |
465 | * validation check fails, otherwise `null`. | |
466 | */ | |
467 | static passwordPolicy( | |
468 | userServiceThis: any, | |
469 | usernameFn?: Function, | |
470 | callback?: (valid: boolean, credits?: number, valuation?: string) => void | |
471 | ): AsyncValidatorFn { | |
472 | return (control: AbstractControl): Observable<ValidationErrors | null> => { | |
473 | if (control.pristine || control.value === '') { | |
474 | if (_.isFunction(callback)) { | |
475 | callback(true, 0); | |
476 | } | |
477 | return observableOf(null); | |
478 | } | |
479 | let username; | |
480 | if (_.isFunction(usernameFn)) { | |
481 | username = usernameFn(); | |
482 | } | |
483 | return observableTimer(500).pipe( | |
484 | switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)), | |
485 | map((resp: { valid: boolean; credits: number; valuation: string }) => { | |
486 | if (_.isFunction(callback)) { | |
487 | callback(resp.valid, resp.credits, resp.valuation); | |
488 | } | |
489 | if (resp.valid) { | |
490 | return null; | |
491 | } else { | |
492 | return { passwordPolicy: true }; | |
493 | } | |
494 | }), | |
495 | take(1) | |
496 | ); | |
497 | }; | |
498 | } | |
a4b75251 TL |
499 | |
500 | /** | |
501 | * Validate the bucket name. In general, bucket names should follow domain | |
502 | * name constraints: | |
503 | * - Bucket names must be unique. | |
504 | * - Bucket names cannot be formatted as IP address. | |
505 | * - Bucket names can be between 3 and 63 characters long. | |
506 | * - Bucket names must not contain uppercase characters or underscores. | |
507 | * - Bucket names must start with a lowercase letter or number. | |
508 | * - Bucket names must be a series of one or more labels. Adjacent | |
509 | * labels are separated by a single period (.). Bucket names can | |
510 | * contain lowercase letters, numbers, and hyphens. Each label must | |
511 | * start and end with a lowercase letter or a number. | |
512 | */ | |
513 | static bucketName(): AsyncValidatorFn { | |
514 | return (control: AbstractControl): Observable<ValidationErrors | null> => { | |
515 | if (control.pristine || !control.value) { | |
516 | return observableOf({ required: true }); | |
517 | } | |
518 | const constraints = []; | |
519 | let errorName: string; | |
520 | // - Bucket names cannot be formatted as IP address. | |
521 | constraints.push(() => { | |
522 | const ipv4Rgx = /^((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; | |
523 | const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; | |
524 | const name = control.value; | |
525 | let notIP = true; | |
526 | if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) { | |
527 | errorName = 'ipAddress'; | |
528 | notIP = false; | |
529 | } | |
530 | return notIP; | |
531 | }); | |
532 | // - Bucket names can be between 3 and 63 characters long. | |
533 | constraints.push((name: string) => { | |
534 | if (!_.inRange(name.length, 3, 64)) { | |
535 | errorName = 'shouldBeInRange'; | |
536 | return false; | |
537 | } | |
538 | // Bucket names can only contain lowercase letters, numbers, periods and hyphens. | |
539 | if (!/^[0-9a-z.-]+$/.test(control.value)) { | |
540 | errorName = 'bucketNameInvalid'; | |
541 | return false; | |
542 | } | |
543 | return true; | |
544 | }); | |
545 | // - Bucket names must not contain uppercase characters or underscores. | |
546 | // - Bucket names must start with a lowercase letter or number. | |
547 | // - Bucket names must be a series of one or more labels. Adjacent | |
548 | // labels are separated by a single period (.). Bucket names can | |
549 | // contain lowercase letters, numbers, and hyphens. Each label must | |
550 | // start and end with a lowercase letter or a number. | |
551 | constraints.push((name: string) => { | |
552 | const labels = _.split(name, '.'); | |
553 | return _.every(labels, (label) => { | |
554 | // Bucket names must not contain uppercase characters or underscores. | |
555 | if (label !== _.toLower(label) || label.includes('_')) { | |
556 | errorName = 'containsUpperCase'; | |
557 | return false; | |
558 | } | |
559 | // Bucket labels can contain lowercase letters, numbers, and hyphens. | |
560 | if (!/^[0-9a-z-]+$/.test(label)) { | |
561 | errorName = 'onlyLowerCaseAndNumbers'; | |
562 | return false; | |
563 | } | |
564 | // Each label must start and end with a lowercase letter or a number. | |
565 | return _.every([0, label.length - 1], (index) => { | |
566 | errorName = 'lowerCaseOrNumber'; | |
567 | return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index])); | |
568 | }); | |
569 | }); | |
570 | }); | |
571 | if (!_.every(constraints, (func: Function) => func(control.value))) { | |
572 | return observableOf( | |
573 | (() => { | |
574 | switch (errorName) { | |
575 | case 'onlyLowerCaseAndNumbers': | |
576 | return { onlyLowerCaseAndNumbers: true }; | |
577 | case 'shouldBeInRange': | |
578 | return { shouldBeInRange: true }; | |
579 | case 'ipAddress': | |
580 | return { ipAddress: true }; | |
581 | case 'containsUpperCase': | |
582 | return { containsUpperCase: true }; | |
583 | case 'lowerCaseOrNumber': | |
584 | return { lowerCaseOrNumber: true }; | |
585 | default: | |
586 | return { bucketNameInvalid: true }; | |
587 | } | |
588 | })() | |
589 | ); | |
590 | } | |
591 | ||
592 | return observableOf(null); | |
593 | }; | |
594 | } | |
595 | ||
596 | static bucketExistence( | |
597 | requiredExistenceResult: boolean, | |
598 | rgwBucketService: RgwBucketService | |
599 | ): AsyncValidatorFn { | |
600 | return (control: AbstractControl): Observable<ValidationErrors | null> => { | |
601 | if (control.pristine || !control.value) { | |
602 | return observableOf({ required: true }); | |
603 | } | |
604 | return rgwBucketService | |
605 | .exists(control.value) | |
606 | .pipe( | |
607 | map((existenceResult: boolean) => | |
608 | existenceResult === requiredExistenceResult ? null : { bucketNameNotAllowed: true } | |
609 | ) | |
610 | ); | |
611 | }; | |
612 | } | |
11fdf7f2 | 613 | } |