001/* 002 * Copyright 2022-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2022-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2022-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.matchingrules; 037 038 039 040import com.unboundid.asn1.ASN1OctetString; 041import com.unboundid.ldap.sdk.LDAPException; 042import com.unboundid.ldap.sdk.ResultCode; 043import com.unboundid.util.NotNull; 044import com.unboundid.util.ThreadSafety; 045import com.unboundid.util.ThreadSafetyLevel; 046 047import static com.unboundid.ldap.matchingrules.MatchingRuleMessages.*; 048 049 050 051/** 052 * This enum defines the policy that the {@link TelephoneNumberMatchingRule} 053 * should use when validating values in accordance with the syntax. Regardless 054 * of the validation policy, the normalized representation of a value will be 055 * the provided value, converted to lowercase, with only spaces and hyphens 056 * removed. 057 */ 058@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 059public enum TelephoneNumberValidationPolicy 060{ 061 /** 062 * A policy that indicates that any non-empty printable string will be 063 * accepted. Neither empty strings nor strings that contain characters from 064 * outside the set of printable characters will be accepted. 065 */ 066 ALLOW_NON_EMPTY_PRINTABLE_STRING, 067 068 069 070 /** 071 * A policy that indicates that any non-empty printable string will be 072 * accepted, as long as it contains at least one digit. Neither empty 073 * strings, strings nor strings that contain characters from outside the set 074 * of printable characters, nor strings without any digits will be accepted. 075 */ 076 ALLOW_NON_EMPTY_PRINTABLE_STRING_WITH_AT_LEAST_ONE_DIGIT, 077 078 079 080 /** 081 * A policy that indicates that only values that strictly adhere to the 082 * X.520 specification will be accepted. Only values that start with a 083 * plus sign, contain at least one digit, and contain only digits, spaces, and 084 * hyphens will be accepted. 085 */ 086 ENFORCE_STRICT_X520_COMPLIANCE; 087 088 089 090 /** 091 * Validates the provided value to ensure that it satisfies this validation 092 * policy. 093 * 094 * @param value The value to be validated. It must not be 095 * {@code null}. 096 * @param isSubstring Indicates whether the provided value represents a 097 * substring rather than a complete value. 098 * 099 * @throws LDAPException If the provided value is not acceptable as per the 100 * constraints of this policy. 101 */ 102 public void validateValue(@NotNull final ASN1OctetString value, 103 final boolean isSubstring) 104 throws LDAPException 105 { 106 switch (this) 107 { 108 case ALLOW_NON_EMPTY_PRINTABLE_STRING: 109 validateNonEmptyPrintableString(value, isSubstring, false); 110 break; 111 112 case ALLOW_NON_EMPTY_PRINTABLE_STRING_WITH_AT_LEAST_ONE_DIGIT: 113 validateNonEmptyPrintableString(value, isSubstring, true); 114 break; 115 116 case ENFORCE_STRICT_X520_COMPLIANCE: 117 default: 118 validateX520Compliant(value, isSubstring); 119 break; 120 } 121 } 122 123 124 125 /** 126 * Validates that the provided value is valid in accordance with a policy that 127 * requires a non-empty printable string. 128 * 129 * @param value The value to be validated. It must not be 130 * {@code null}. 131 * @param isSubstring Indicates whether the value represents a 132 * substring rather than a complete value. 133 * @param requireAtLeastOneDigit Indicates whether to require the value to 134 * contain at least one digit. This only 135 * applies if {@code isSubstring} is false. 136 * 137 * @throws LDAPException If the provided value is not valid. 138 */ 139 private static void validateNonEmptyPrintableString( 140 @NotNull final ASN1OctetString value, 141 final boolean isSubstring, 142 final boolean requireAtLeastOneDigit) 143 throws LDAPException 144 { 145 // Make sure that the value is not empty. 146 final byte[] valueBytes = value.getValue(); 147 if (valueBytes.length == 0) 148 { 149 if (isSubstring) 150 { 151 // Substring components must not be empty. 152 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 153 ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_SUBSTRING.get()); 154 } 155 else 156 { 157 // Telephone number values must not be empty. 158 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 159 ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_VALUE.get()); 160 } 161 } 162 163 164 // Iterate through the bytes of the value and make sure they are all 165 // printable. Also, check to see if we find any digits. 166 boolean digitFound = false; 167 for (int i=0; i < valueBytes.length; i++) 168 { 169 final byte b = valueBytes[i]; 170 if ((b >= '0') && (b <= '9')) 171 { 172 // It's a numeric digit, which is always allowed. 173 digitFound = true; 174 } 175 else if (((b >= 'a') && (b <= 'z')) || ((b >= 'A') && (b <= 'Z'))) 176 { 177 // It's an alphabetic character, which is allowed as per the policy. 178 } 179 else 180 { 181 switch (b) 182 { 183 case '\'': 184 case '(': 185 case ')': 186 case '+': 187 case ',': 188 case '-': 189 case '.': 190 case '=': 191 case '/': 192 case ':': 193 case '?': 194 case ' ': 195 // These characters are all allowed. 196 break; 197 default: 198 // This character is not allowed. 199 if (isSubstring) 200 { 201 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 202 ERR_TELEPHONE_NUMBER_VALIDATION_NON_PRINTABLE_SUB_CHAR.get( 203 value.stringValue(), i)); 204 } 205 else 206 { 207 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 208 ERR_TELEPHONE_NUMBER_VALIDATION_NON_PRINTABLE_CHAR.get( 209 value.stringValue(), i)); 210 } 211 } 212 } 213 } 214 215 216 // If we should require a digit, then make sure we found one. 217 if (requireAtLeastOneDigit && (! isSubstring) && (! digitFound)) 218 { 219 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 220 ERR_TELEPHONE_NUMBER_VALIDATION_NO_DIGITS.get(value.stringValue())); 221 } 222 } 223 224 225 226 /** 227 * Validates that the provided value is a valid telephone number using the 228 * strict specification defined in X.520. 229 * 230 * @param value The value to be validated. It must not be 231 * {@code null}. 232 * @param isSubstring Indicates whether the value represents a substring 233 * rather than a complete value. 234 * 235 * @throws LDAPException If the provided value is not valid. 236 */ 237 private static void validateX520Compliant( 238 @NotNull final ASN1OctetString value, 239 final boolean isSubstring) 240 throws LDAPException 241 { 242 // Make sure that the value is not empty. 243 final byte[] valueBytes = value.getValue(); 244 if (valueBytes.length == 0) 245 { 246 if (isSubstring) 247 { 248 // Substring components must not be empty. 249 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 250 ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_SUBSTRING.get()); 251 } 252 else 253 { 254 // Telephone number values must not be empty. 255 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 256 ERR_TELEPHONE_NUMBER_VALIDATION_EMPTY_VALUE.get()); 257 } 258 } 259 260 261 // If the value is not a substring, then make sure it starts with a plus 262 // sign. 263 if ((! isSubstring) && (valueBytes[0] != '+')) 264 { 265 throw new LDAPException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 266 ERR_TELEPHONE_NUMBER_VALIDATION_MISSING_PLUS.get( 267 value.stringValue())); 268 } 269 270 271 // Iterate through the bytes of the value and make sure it only contains a 272 // plus sign, one or more numeric digits, and optionally spaces and/or 273 // dashes. A plus sign will only be allowed at position zero, and digits, 274 // spaces, and hyphens will be allowed anywhere. 275 boolean digitFound = false; 276 for (int i=0; i < valueBytes.length; i++) 277 { 278 final byte b = valueBytes[i]; 279 if (b == '+') 280 { 281 if (i != 0) 282 { 283 if (isSubstring) 284 { 285 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 286 ERR_TELEPHONE_NUMBER_VALIDATION_NON_FIRST_PLUS_SUB.get( 287 value.stringValue(), i)); 288 } 289 else 290 { 291 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 292 ERR_TELEPHONE_NUMBER_VALIDATION_NON_FIRST_PLUS.get( 293 value.stringValue(), i)); 294 } 295 } 296 } 297 else if ((b >= '0') && (b <= '9')) 298 { 299 digitFound = true; 300 } 301 else if ((b == ' ') || (b == '-')) 302 { 303 // These are always allowed and always ignored. 304 } 305 else 306 { 307 if (isSubstring) 308 { 309 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 310 ERR_TELEPHONE_NUMBER_VALIDATION_INVALID_CHAR_SUB.get( 311 value.stringValue(), i)); 312 } 313 else 314 { 315 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 316 ERR_TELEPHONE_NUMBER_VALIDATION_INVALID_CHAR.get( 317 value.stringValue(), i)); 318 } 319 } 320 } 321 322 323 // If we didn't find any digits, then that's an error, even for a substring 324 // assertion. 325 if (! digitFound) 326 { 327 if (isSubstring) 328 { 329 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 330 ERR_TELEPHONE_NUMBER_VALIDATION_NO_DIGITS_SUB.get( 331 value.stringValue())); 332 } 333 else 334 { 335 throw new LDAPException(ResultCode.INAPPROPRIATE_MATCHING, 336 ERR_TELEPHONE_NUMBER_VALIDATION_NO_DIGITS.get( 337 value.stringValue())); 338 } 339 } 340 } 341}