343 lines
13 KiB
C#
343 lines
13 KiB
C#
using System.Net;
|
|
using System.Net.Mail;
|
|
using System.Text;
|
|
|
|
namespace SGU.AuthBroker.Core.Profiles;
|
|
|
|
public static class SguProfileParser
|
|
{
|
|
private const string AdministrativeNameId = "ctl00_contenedor_decEncabezado_lblNombre";
|
|
private const string EmployeeTypeId = "ctl00_contenedor_decEncabezado_lblIndicadorValue";
|
|
private const string EmailId = "ctl00_contenedor_decEncabezado_lblCorreo";
|
|
private const string JobTitleId = "ctl00_contenedor_decEncabezado_lblPuesto";
|
|
private const string DepartmentId = "ctl00_contenedor_decEncabezado_lblDependencia";
|
|
private const string MenuNameId = "ctl00_lblNombreUsuario";
|
|
private const string StudentNumberId = "ctl00_contenedor_HistorialAlumno1_lblClaveAlumnoHP";
|
|
private const string StudentGivenNameId = "ctl00_contenedor_HistorialAlumno1_lblNombreAlumnoHP";
|
|
private const string StudentPaternalSurnameId = "ctl00_contenedor_HistorialAlumno1_lblApPatAlumnoHP";
|
|
private const string StudentMaternalSurnameId = "ctl00_contenedor_HistorialAlumno1_lblApMatAlumnoHP";
|
|
private const string StudentEmailId = "ctl00_contenedor_HistorialAlumno1_lblCorreoAlumnoHP";
|
|
private const string StudentCareerId = "ctl00_contenedor_HistorialAlumno1_Header1_lblCarrera";
|
|
private const string StudentStreetId = "ctl00_contenedor_HistorialAlumno1_lblDomicilioAlumnoHP";
|
|
private const string StudentNeighborhoodId = "ctl00_contenedor_HistorialAlumno1_lblColoniaAlumnoHP";
|
|
private const string StudentStateId = "ctl00_contenedor_HistorialAlumno1_lblEstadoAlumnoHP";
|
|
private const string StudentCityId = "ctl00_contenedor_HistorialAlumno1_lblCiudadAlumnoHP";
|
|
private const string StudentMunicipalityId = "ctl00_contenedor_HistorialAlumno1_lblDeloMunAlumnoHP";
|
|
private const string StudentPostalCodeId = "ctl00_contenedor_HistorialAlumno1_lblCPAlumnoHP";
|
|
|
|
public static InstitutionalProfile? ParseAdministrative(string html, string expectedEmployeeNumber)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(html);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(expectedEmployeeNumber);
|
|
|
|
string? identity = ExtractSpanText(html, AdministrativeNameId);
|
|
if (!TrySplitAdministrativeIdentity(identity, out string? employeeNumber, out string? displayName) ||
|
|
!string.Equals(employeeNumber, expectedEmployeeNumber, StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
InstitutionalProfile profile = new(
|
|
EmployeeNumber: employeeNumber,
|
|
DisplayName: NormalizeTitle(displayName, 256),
|
|
Email: NormalizeEmail(ExtractSpanText(html, EmailId)),
|
|
EmployeeType: NormalizeSentence(ExtractSpanText(html, EmployeeTypeId), 256),
|
|
JobTitle: NormalizeTitle(ExtractSpanText(html, JobTitleId), 64),
|
|
Department: NormalizeTitle(ExtractSpanText(html, DepartmentId), 64));
|
|
return profile.HasValues ? profile : null;
|
|
}
|
|
|
|
public static InstitutionalProfile? ParseMenu(string html)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(html);
|
|
|
|
InstitutionalProfile profile = new(
|
|
DisplayName: NormalizeTitle(ExtractSpanText(html, MenuNameId), 256));
|
|
return profile.HasValues ? profile : null;
|
|
}
|
|
|
|
public static InstitutionalProfile? ParseStudent(string html, string expectedStudentNumber)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(html);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(expectedStudentNumber);
|
|
|
|
string? studentNumber = NormalizeInstitutionalNumber(ExtractSpanText(html, StudentNumberId));
|
|
if (!string.Equals(studentNumber, expectedStudentNumber, StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string? givenName = NormalizeName(ExtractSpanText(html, StudentGivenNameId), 64);
|
|
string? paternalSurname = NormalizeSurname(ExtractSpanText(html, StudentPaternalSurnameId), 64);
|
|
string? maternalSurname = NormalizeSurname(ExtractSpanText(html, StudentMaternalSurnameId), 64);
|
|
string? surname = NormalizeSurname(JoinNonEmpty(" ", paternalSurname, maternalSurname), 64);
|
|
string? displayName = NormalizeName(JoinNonEmpty(" ", givenName, surname), 256);
|
|
|
|
string? street = NormalizeTitle(ExtractSpanText(html, StudentStreetId), 512);
|
|
string? neighborhood = NormalizeTitle(ExtractSpanText(html, StudentNeighborhoodId), 256);
|
|
string? city = NormalizeTitle(ExtractSpanText(html, StudentCityId), 128);
|
|
string? municipality = NormalizeTitle(ExtractSpanText(html, StudentMunicipalityId), 128);
|
|
string? streetAddress = BuildStreetAddress(street, neighborhood, municipality, city);
|
|
|
|
InstitutionalProfile profile = new(
|
|
EmployeeNumber: studentNumber,
|
|
DisplayName: displayName,
|
|
GivenName: givenName,
|
|
Surname: surname,
|
|
Email: NormalizeEmail(ExtractSpanText(html, StudentEmailId)),
|
|
EmployeeType: "Alumno",
|
|
JobTitle: BuildStudentJobTitle(ExtractSpanText(html, StudentCareerId)),
|
|
StreetAddress: streetAddress,
|
|
City: city ?? municipality,
|
|
State: NormalizeTitle(ExtractSpanText(html, StudentStateId), 128),
|
|
PostalCode: NormalizePostalCode(ExtractSpanText(html, StudentPostalCodeId)));
|
|
return profile.HasValues ? profile : null;
|
|
}
|
|
|
|
private static bool TrySplitAdministrativeIdentity(
|
|
string? value,
|
|
out string? employeeNumber,
|
|
out string? displayName)
|
|
{
|
|
employeeNumber = null;
|
|
displayName = null;
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int separator = value.IndexOf(" - ", StringComparison.Ordinal);
|
|
if (separator != 6)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string candidateNumber = value[..separator];
|
|
string candidateName = value[(separator + 3)..].Trim();
|
|
if (candidateNumber.Length != 6 ||
|
|
!candidateNumber.All(char.IsAsciiDigit) ||
|
|
string.IsNullOrWhiteSpace(candidateName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
employeeNumber = candidateNumber;
|
|
displayName = candidateName;
|
|
return true;
|
|
}
|
|
|
|
private static string? ExtractSpanText(string html, string id)
|
|
{
|
|
foreach (char quote in new[] { '"', '\'' })
|
|
{
|
|
string marker = $"id={quote}{id}{quote}";
|
|
int searchFrom = 0;
|
|
while (searchFrom < html.Length)
|
|
{
|
|
int idIndex = html.IndexOf(marker, searchFrom, StringComparison.OrdinalIgnoreCase);
|
|
if (idIndex < 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
int spanStart = html.LastIndexOf("<span", idIndex, StringComparison.OrdinalIgnoreCase);
|
|
int precedingTagEnd = html.LastIndexOf('>', idIndex);
|
|
if (spanStart > precedingTagEnd)
|
|
{
|
|
int openingTagEnd = html.IndexOf('>', idIndex);
|
|
int closingTagStart = openingTagEnd < 0
|
|
? -1
|
|
: html.IndexOf("</span", openingTagEnd + 1, StringComparison.OrdinalIgnoreCase);
|
|
if (openingTagEnd >= 0 && closingTagStart >= 0)
|
|
{
|
|
string innerHtml = html[(openingTagEnd + 1)..closingTagStart];
|
|
return NormalizeText(innerHtml);
|
|
}
|
|
}
|
|
|
|
searchFrom = idIndex + marker.Length;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? NormalizeText(string htmlFragment)
|
|
{
|
|
StringBuilder withoutTags = new(htmlFragment.Length);
|
|
bool insideTag = false;
|
|
foreach (char character in htmlFragment)
|
|
{
|
|
if (character == '<')
|
|
{
|
|
insideTag = true;
|
|
}
|
|
else if (character == '>')
|
|
{
|
|
insideTag = false;
|
|
}
|
|
else if (!insideTag)
|
|
{
|
|
withoutTags.Append(character);
|
|
}
|
|
}
|
|
|
|
string decoded = WebUtility.HtmlDecode(withoutTags.ToString());
|
|
StringBuilder normalized = new(decoded.Length);
|
|
bool previousWasWhitespace = true;
|
|
foreach (char character in decoded)
|
|
{
|
|
bool whitespace = char.IsWhiteSpace(character) || character == '\u00A0';
|
|
if (whitespace)
|
|
{
|
|
if (!previousWasWhitespace)
|
|
{
|
|
normalized.Append(' ');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
normalized.Append(character);
|
|
}
|
|
|
|
previousWasWhitespace = whitespace;
|
|
}
|
|
|
|
string result = normalized.ToString().Trim();
|
|
return result.Length == 0 ? null : result;
|
|
}
|
|
|
|
private static string? NormalizeEmail(string? value)
|
|
{
|
|
string? candidate = Limit(value, 256);
|
|
if (candidate is null ||
|
|
!MailAddress.TryCreate(candidate, out MailAddress? address) ||
|
|
!string.Equals(address.Address, candidate, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return address.Address.ToLowerInvariant();
|
|
}
|
|
|
|
private static string? NormalizeTitle(string? value, int maximumLength)
|
|
{
|
|
string? candidate = Limit(value, maximumLength);
|
|
return candidate is null ? null : SpanishTextNormalizer.ToTitleCase(candidate);
|
|
}
|
|
|
|
private static string? NormalizeName(string? value, int maximumLength)
|
|
{
|
|
string? candidate = Limit(value, maximumLength);
|
|
return candidate is null ? null : SpanishTextNormalizer.ToTitleCase(candidate);
|
|
}
|
|
|
|
private static string? NormalizeSurname(string? value, int maximumLength)
|
|
{
|
|
string? candidate = Limit(value, maximumLength);
|
|
return candidate is null ? null : SpanishTextNormalizer.ToSurnameCase(candidate);
|
|
}
|
|
|
|
private static string? NormalizeInstitutionalNumber(string? value)
|
|
{
|
|
string? candidate = value?.Trim();
|
|
return candidate is { Length: 6 } && candidate.All(char.IsAsciiDigit)
|
|
? candidate
|
|
: null;
|
|
}
|
|
|
|
private static string? BuildStudentJobTitle(string? value)
|
|
{
|
|
string? candidate = Limit(value, 256);
|
|
if (candidate is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
candidate = candidate.Replace(
|
|
"COMPUTACIO NALES",
|
|
"COMPUTACIONALES",
|
|
StringComparison.OrdinalIgnoreCase);
|
|
string career = SpanishTextNormalizer.ToTitleCase(candidate);
|
|
const string degreePrefix = "Licenciatura en ";
|
|
if (career.StartsWith(degreePrefix, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
career = career[degreePrefix.Length..];
|
|
}
|
|
|
|
return TruncateAtWordBoundary($"Estudiante de {career}", 64);
|
|
}
|
|
|
|
private static string? BuildStreetAddress(
|
|
string? street,
|
|
string? neighborhood,
|
|
string? municipality,
|
|
string? city)
|
|
{
|
|
List<string> lines = [];
|
|
AddDistinct(lines, street);
|
|
AddDistinct(lines, neighborhood);
|
|
if (!string.Equals(municipality, city, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
AddDistinct(lines, municipality);
|
|
}
|
|
|
|
return Limit(string.Join("\r\n", lines), 1024);
|
|
}
|
|
|
|
private static void AddDistinct(List<string> values, string? candidate)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(candidate) &&
|
|
!values.Contains(candidate, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
values.Add(candidate);
|
|
}
|
|
}
|
|
|
|
private static string? NormalizePostalCode(string? value)
|
|
{
|
|
string? candidate = value?.Trim();
|
|
if (candidate is null ||
|
|
candidate.Length is < 4 or > 5 ||
|
|
!candidate.All(char.IsAsciiDigit))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return candidate.PadLeft(5, '0');
|
|
}
|
|
|
|
private static string? JoinNonEmpty(string separator, params string?[] values)
|
|
{
|
|
string result = string.Join(separator, values.Where(value => !string.IsNullOrWhiteSpace(value)));
|
|
return string.IsNullOrWhiteSpace(result) ? null : result;
|
|
}
|
|
|
|
private static string TruncateAtWordBoundary(string value, int maximumLength)
|
|
{
|
|
if (value.Length <= maximumLength)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
int boundary = value.LastIndexOf(' ', maximumLength - 1, maximumLength);
|
|
return value[..(boundary > 0 ? boundary : maximumLength)].TrimEnd();
|
|
}
|
|
|
|
private static string? NormalizeSentence(string? value, int maximumLength)
|
|
{
|
|
string? candidate = Limit(value, maximumLength);
|
|
return candidate is null ? null : SpanishTextNormalizer.ToSentenceCase(candidate);
|
|
}
|
|
|
|
private static string? Limit(string? value, int maximumLength)
|
|
{
|
|
string? candidate = value?.Trim();
|
|
return string.IsNullOrEmpty(candidate) ||
|
|
candidate.Length > maximumLength ||
|
|
candidate.Contains('\uFFFD')
|
|
? null
|
|
: candidate;
|
|
}
|
|
}
|