1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 namespace System.ComponentModel.DataAnnotations
6 {
7     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
8         AllowMultiple = false)]
9     public sealed class PhoneAttribute : DataTypeAttribute
10     {
11         private const string AdditionalPhoneNumberCharacters = "-.()";
12         private const string ExtensionAbbreviationExtDot = "ext.";
13         private const string ExtensionAbbreviationExt = "ext";
14         private const string ExtensionAbbreviationX = "x";
15 
PhoneAttribute()16         public PhoneAttribute()
17             : base(DataType.PhoneNumber)
18         {
19             // Set DefaultErrorMessage not ErrorMessage, allowing user to set
20             // ErrorMessageResourceType and ErrorMessageResourceName to use localized messages.
21             DefaultErrorMessage = SR.PhoneAttribute_Invalid;
22         }
23 
IsValid(object value)24         public override bool IsValid(object value)
25         {
26             if (value == null)
27             {
28                 return true;
29             }
30 
31             if (!(value is string valueAsString))
32             {
33                 return false;
34             }
35 
36             valueAsString = valueAsString.Replace("+", string.Empty).TrimEnd();
37             valueAsString = RemoveExtension(valueAsString);
38 
39             bool digitFound = false;
40             foreach (char c in valueAsString)
41             {
42                 if (Char.IsDigit(c))
43                 {
44                     digitFound = true;
45                     break;
46                 }
47             }
48 
49             if (!digitFound)
50             {
51                 return false;
52             }
53 
54             foreach (char c in valueAsString)
55             {
56                 if (!(Char.IsDigit(c)
57                     || Char.IsWhiteSpace(c)
58                     || AdditionalPhoneNumberCharacters.IndexOf(c) != -1))
59                 {
60                     return false;
61                 }
62             }
63 
64             return true;
65         }
66 
RemoveExtension(string potentialPhoneNumber)67         private static string RemoveExtension(string potentialPhoneNumber)
68         {
69             var lastIndexOfExtension = potentialPhoneNumber
70                 .LastIndexOf(ExtensionAbbreviationExtDot, StringComparison.OrdinalIgnoreCase);
71             if (lastIndexOfExtension >= 0)
72             {
73                 var extension = potentialPhoneNumber.Substring(
74                     lastIndexOfExtension + ExtensionAbbreviationExtDot.Length);
75                 if (MatchesExtension(extension))
76                 {
77                     return potentialPhoneNumber.Substring(0, lastIndexOfExtension);
78                 }
79             }
80 
81             lastIndexOfExtension = potentialPhoneNumber
82                 .LastIndexOf(ExtensionAbbreviationExt, StringComparison.OrdinalIgnoreCase);
83             if (lastIndexOfExtension >= 0)
84             {
85                 var extension = potentialPhoneNumber.Substring(
86                     lastIndexOfExtension + ExtensionAbbreviationExt.Length);
87                 if (MatchesExtension(extension))
88                 {
89                     return potentialPhoneNumber.Substring(0, lastIndexOfExtension);
90                 }
91             }
92 
93             lastIndexOfExtension = potentialPhoneNumber
94                 .LastIndexOf(ExtensionAbbreviationX, StringComparison.OrdinalIgnoreCase);
95             if (lastIndexOfExtension >= 0)
96             {
97                 var extension = potentialPhoneNumber.Substring(
98                     lastIndexOfExtension + ExtensionAbbreviationX.Length);
99                 if (MatchesExtension(extension))
100                 {
101                     return potentialPhoneNumber.Substring(0, lastIndexOfExtension);
102                 }
103             }
104 
105             return potentialPhoneNumber;
106         }
107 
MatchesExtension(string potentialExtension)108         private static bool MatchesExtension(string potentialExtension)
109         {
110             potentialExtension = potentialExtension.TrimStart();
111             if (potentialExtension.Length == 0)
112             {
113                 return false;
114             }
115 
116             foreach (char c in potentialExtension)
117             {
118                 if (!Char.IsDigit(c))
119                 {
120                     return false;
121                 }
122             }
123 
124             return true;
125         }
126     }
127 }
128