using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Data; using System.Net; using System.Web; using System.Web.UI.WebControls; using CMS.UIControls; using CMS.GlobalHelper; using CMS.SettingsProvider; using CMS.CMSHelper; using CMS.IO; using CMS.ExtendedControls.ActionsConfig; using CMS.FormControls; using CMS.ExtendedControls; using HtmlAgilityPack; public partial class CMSAdminControls_Validation_AccessibilityValidator : DocumentValidator { #region "Accessibility standard enum" /// /// Accessibility standard enum /// public enum AccessibilityStandardEnum : int { /// /// BIK BITV 1 /// BITV1_0 = 1, /// /// Section 508 /// Section508 = 2, /// /// Stanca Act /// StancaAct = 3, /// /// WCAG 1 A /// WCAG1_0A = 4, /// /// WCAG 1 AA /// WCAG1_0AA = 5, /// /// WCAG 1 AAA /// WCAG1_0AAA = 6, /// /// WCAG 2 A /// WCAG2_0A = 7, /// /// WCAG 2 AA /// WCAG2_0AA = 8, /// /// WCAG 2 AAA /// WCAG2_0AAA = 9 } #endregion #region "Accessibility code" /// /// Accessibility standard code /// public static class AccessibilityStandardCode { #region "Constants" /// /// BIK BITV 1 /// public const int BITV1_0 = 1; /// /// Section 508 /// public const int Section508 = 2; /// /// Stanca Act /// public const int StancaAct = 3; /// /// WCAG 1 A /// public const int WCAG1_0A = 4; /// /// WCAG 1 AA /// public const int WCAG1_0AA = 5; /// /// WCAG 1 AAA /// public const int WCAG1_0AAA = 6; /// /// WCAG 2 A /// public const int WCAG2_0A = 7; /// /// WCAG 2 AA /// public const int WCAG2_0AA = 8; /// /// WCAG 2 AAA /// public const int WCAG2_0AAA = 9; #endregion #region "Methods" /// /// Returns the enumeration representation of the Accessibility standard code. /// /// Accessibility standard code public static AccessibilityStandardEnum ToEnum(int code) { switch (code) { case BITV1_0: return AccessibilityStandardEnum.BITV1_0; case Section508: return AccessibilityStandardEnum.Section508; case StancaAct: return AccessibilityStandardEnum.StancaAct; case WCAG1_0A: return AccessibilityStandardEnum.WCAG1_0A; case WCAG1_0AA: return AccessibilityStandardEnum.WCAG1_0AA; case WCAG1_0AAA: return AccessibilityStandardEnum.WCAG1_0AAA; case WCAG2_0A: return AccessibilityStandardEnum.WCAG2_0A; case WCAG2_0AA: return AccessibilityStandardEnum.WCAG2_0AA; case WCAG2_0AAA: return AccessibilityStandardEnum.WCAG2_0AAA; default: return AccessibilityStandardEnum.WCAG2_0AA; } } /// /// Returns the accessibility standard code from the enumeration value. /// /// Value to convert public static int FromEnum(AccessibilityStandardEnum value) { switch (value) { case AccessibilityStandardEnum.BITV1_0: return BITV1_0; case AccessibilityStandardEnum.Section508: return Section508; case AccessibilityStandardEnum.StancaAct: return StancaAct; case AccessibilityStandardEnum.WCAG1_0A: return WCAG1_0A; case AccessibilityStandardEnum.WCAG1_0AA: return WCAG1_0AA; case AccessibilityStandardEnum.WCAG1_0AAA: return WCAG1_0AAA; case AccessibilityStandardEnum.WCAG2_0A: return WCAG2_0A; case AccessibilityStandardEnum.WCAG2_0AA: return WCAG2_0AA; case AccessibilityStandardEnum.WCAG2_0AAA: return WCAG2_0AAA; default: return WCAG2_0AA; } } #endregion } #endregion #region "Constants" private const string DEFAULT_VALIDATOR_URL = "http://achecker.ca/checker/index.php"; #endregion #region "Variables" private string mValidatorURL = null; private bool mUseServerRequest = false; private static DataSet mDataSource = null; private Regex mIntNumberRegEx = null; private DropDownList mStandardList = null; private string mErrorText = null; #endregion #region "Properties" /// /// URL to which validator requests will be sent /// public string ValidatorURL { get { return mValidatorURL ?? (mValidatorURL = DataHelper.GetNotEmpty(SettingsHelper.AppSettings["CMSValidationAccessibilityValidatorURL"], DEFAULT_VALIDATOR_URL)); } set { mValidatorURL = value; } } /// /// Indicates if control is used on live site /// public override bool IsLiveSite { get { return base.IsLiveSite; } set { base.IsLiveSite = value; gridValidationResult.IsLiveSite = value; } } /// /// Indicates if server request will be used rather than JavaScript request to obtain HTML /// public bool UseServerRequestType { get { return mUseServerRequest; } set { mUseServerRequest = value; } } /// /// Key to store validation result /// protected override string ResultKey { get { return "validation|access|" + CultureCode + "|" + Url; } } /// /// Gets or sets source of the data for unigrid control /// public override DataSet DataSource { get { if (mDataSource == null) { mDataSource = base.DataSource; } base.DataSource = mDataSource; return mDataSource; } set { base.DataSource = value; mDataSource = value; } } /// /// Gets or sets validation standard /// public AccessibilityStandardEnum Standard { get; set; } /// /// Regular expression for integer numbers /// private Regex IntNumberRegEx { get { return mIntNumberRegEx ?? (mIntNumberRegEx = RegexHelper.GetRegex("\\b\\d+\\b")); } } /// /// Messages placeholder /// public override MessagesPlaceHolder MessagesPlaceHolder { get { return plcMess; } } #endregion #region "Control methods" /// /// Page load /// protected override void OnInit(EventArgs e) { FormEngineUserControl label = LoadUserControl("~/CMSFormControls/Basic/LabelControl.ascx") as FormEngineUserControl; if (label != null) { label.Value = GetString("validation.accessibility.standard"); } // Add validation standard FormEngineUserControl standard = LoadUserControl("~/CMSFormControls/Basic/DropDownListControl.ascx") as FormEngineUserControl; if (standard != null) { mStandardList = standard.FindControl(standard.InputControlID) as DropDownList; } DataHelper.FillListControlWithEnum(typeof(AccessibilityStandardEnum), mStandardList, "validation.accessibility.standard.", null); // Set default standard value if (!RequestHelper.IsPostBack() && (standard != null)) { standard.Value = AccessibilityStandardCode.FromEnum(AccessibilityStandardEnum.WCAG2_0A); } HeaderActions.AdditionalControls.Add(label); HeaderActions.AdditionalControls.Add(standard); HeaderActions.AdditionalControlsCssClass = "HeaderActionsLabel"; HeaderActions.ReloadAdditionalControls(); } /// /// Page load /// protected void Page_Load(object sender, EventArgs e) { bool isPostBack = RequestHelper.IsPostBack(); if (!isPostBack) { DataSource = null; } ReloadData(isPostBack); } /// /// Page PreRender /// protected void Page_PreRender(object sender, EventArgs e) { if (!string.IsNullOrEmpty(mErrorText)) { ShowError(mErrorText); } } /// /// Initializes all nested controls. /// private void SetupControls() { IsLiveSite = false; InitializeScripts(); HeaderActions.ActionsList.Clear(); // Validate action HeaderAction validate = new HeaderAction(); validate.ControlType = HeaderActionTypeEnum.LinkButton; validate.OnClientClick = "LoadHTMLToElement('" + hdnHTML.ClientID + "'," + ScriptHelper.GetString(Url) + ");"; validate.Text = GetString("general.validate"); validate.Tooltip = validate.Text; validate.ImageUrl = GetImageUrl("Design/Controls/Validation/checks.png"); validate.CommandName = "validate"; // View HTML code string click = GetViewSourceActionClick(); HeaderAction viewCode = new HeaderAction(); viewCode.ControlType = HeaderActionTypeEnum.Hyperlink; viewCode.OnClientClick = click; viewCode.Text = GetString("validation.viewcode"); viewCode.Tooltip = viewCode.Text; viewCode.ImageUrl = GetImageUrl("Design/Controls/Validation/codeview.png"); // Show results in new window HeaderAction newWindow = new HeaderAction(); newWindow.ControlType = HeaderActionTypeEnum.Hyperlink; newWindow.OnClientClick = click; newWindow.Text = GetString("validation.showresultsnewwindow"); newWindow.Tooltip = newWindow.Text; if (DataHelper.DataSourceIsEmpty(DataSource)) { newWindow.Enabled = false; newWindow.OnClientClick = null; newWindow.ImageUrl = GetImageUrl("Design/Controls/Validation/windownewdisabled.png"); } else { newWindow.Enabled = true; string encodedKey = ScriptHelper.GetString(HttpUtility.UrlEncode(ResultKey), false); newWindow.OnClientClick = String.Format("modalDialog('" + ResolveUrl("~/CMSModules/Content/CMSDesk/Validation/ValidationResults.aspx") + "?datakey={0}&docid={1}&hash={2}', 'ViewValidationResult', 800, 600);return false;", encodedKey, Node.DocumentID, QueryHelper.GetHash(String.Format("?datakey={0}&docid={1}", encodedKey, Node.DocumentID))); newWindow.ImageUrl = GetImageUrl("Design/Controls/Validation/windownew.png"); } HeaderActions.AddAction(validate); HeaderActions.AddAction(viewCode); HeaderActions.AddAction(newWindow); HeaderActions.HelpName = "helpTopic"; HeaderActions.HelpTopicName = "accessibilityvalidator"; HeaderActions.ActionPerformed += HeaderActions_ActionPerformed; // Set sorting and add events gridValidationResult.OrderBy = "line"; gridValidationResult.IsLiveSite = IsLiveSite; gridValidationResult.OnExternalDataBound += gridValidationResult_OnExternalDataBound; gridValidationResult.OnDataReload += gridValidationResult_OnDataReload; gridValidationResult.ZeroRowsText = GetString("validation.access.notvalidated"); gridValidationResult.ShowActionsMenu = true; gridValidationResult.AllColumns = "line, column, accessibilityrule, error, fixsuggestion, source"; // Set custom validating text up.ProgressHTML = String.Concat("
", String.Concat("\"",", GetString("validation.validating"), ""), "
"); mStandardList.CssClass = ""; } /// /// Actions handler. /// protected void HeaderActions_ActionPerformed(object sender, CommandEventArgs e) { switch (e.CommandName) { case "validate": Standard = AccessibilityStandardCode.ToEnum(ValidationHelper.GetInteger(((FormEngineUserControl)HeaderActions.AdditionalControls[1]).Value, 0)); Validate(); break; } } /// /// Actions handler. /// private void Validate() { DataSource = null; DataSource = ValidateHtml(); ReloadData(true); } /// /// Loads data from the data source property. /// public void ReloadData(bool force) { SetupControls(); gridValidationResult.ReloadData(); if (force) { ProcessResult(DataSource); } } /// /// Data reload event /// protected DataSet gridValidationResult_OnDataReload(string completeWhere, string currentOrder, int currentTopN, string columns, int currentOffset, int currentPageSize, ref int totalRecords) { if (!DataHelper.DataSourceIsEmpty(DataSource)) { totalRecords = DataSource.Tables[0].Rows.Count; } return DataSource; } /// /// On external data bound event /// /// Sender /// Action what is called /// Parameter /// Result object protected object gridValidationResult_OnExternalDataBound(object sender, string sourceName, object parameter) { return GridExternalDataBound(sender, sourceName, parameter); } #endregion #region "Validation methods" /// /// Get HTML code using server or client method /// /// URL to obtain HTML from private string GetHtml(string url) { if (UseServerRequestType) { // Create web client and try to obtain HTML using it WebClient client = new WebClient(); try { StreamReader reader = StreamReader.New(client.OpenRead(url)); return reader.ReadToEnd(); } catch (Exception e) { mErrorText = String.Format(ResHelper.GetString("validation.exception"), e.Message); return null; } } else { // Get HTML stored using JavaScript return ValidationHelper.Base64Decode(hdnHTML.Value); } } /// /// Send validation request to validator and obtain result /// /// HTML to validate /// DataSet containing validator response private DataSet GetValidationResult(string htmlData) { try { DataSet dsValResult = null; Random randGen = new Random(); // Create web request HttpWebRequest req = (HttpWebRequest)WebRequest.Create(ValidatorURL); req.Method = "POST"; string boundary = "---------------------------" + randGen.Next(1000000, 9999999) + randGen.Next(1000000, 9999999); req.ContentType = "multipart/form-data; boundary=" + boundary; // Set data to web request for validation byte[] data = Encoding.GetEncoding("UTF-8").GetBytes(GetRequestData(GetRequestDictionary(htmlData), boundary)); req.ContentLength = data.Length; StreamWrapper writer = StreamWrapper.New(req.GetRequestStream()); writer.Write(data, 0, data.Length); writer.Close(); // Process server answer StreamWrapper answer = StreamWrapper.New(req.GetResponse().GetResponseStream()); if (answer != null) { // Load document with given HTML HtmlDocument doc = new HtmlDocument(); doc.Load(answer.SystemStream); // Find the div node with validation results var divErrors = doc.DocumentNode.SelectSingleNode("//div[@id='AC_errors']"); DataTable dtErrors = CreateAccessibilityTable("errors", divErrors); //var divWarnings = doc.DocumentNode.SelectSingleNode("//div[@id='AC_likely_problems']"); //var divPotential = doc.DocumentNode.SelectSingleNode("//div[@id='AC_potential_problems']"); dsValResult = new DataSet(); dsValResult.Tables.Add(dtErrors); } return dsValResult; } catch { mErrorText = GetString("validation.servererror"); return null; } } /// /// Create accessibility table with validation results /// /// Name of the result table /// HTML node containing validation results private DataTable CreateAccessibilityTable(string tableName, HtmlNode node) { DataTable tb = null; if (node != null) { // Create table to store results tb = new DataTable(tableName); tb.Columns.AddRange(new DataColumn[] { new DataColumn("line",typeof(int)), new DataColumn("column",typeof(int)), new DataColumn("accessibilityrule",typeof(string)), new DataColumn("error",typeof(string)), new DataColumn("fixsuggestion",typeof(string)), new DataColumn("source",typeof(string)) }); // Variables to store data string mainRule = null; string minorRule = null; string error = null; string fix = null; int line = 0; int col = 0; string source = null; // Process specified HTML nodes containing results HtmlNodeCollection nodes = node.SelectNodes("./h3 | ./h4 | ./div[@class='gd_one_check']//span[@class='gd_msg']/a | ./div[@class='gd_one_check']//div[@class='gd_question_section'] | ./div[@class='gd_one_check']//table//tr"); // Go through results if (nodes != null) { foreach (HtmlNode child in nodes) { switch (child.Name.ToLowerCSafe()) { case "h3": mainRule = child.InnerText; break; case "h4": minorRule = child.InnerText; break; case "a": error = child.InnerHtml; break; case "div": fix = child.InnerHtml; break; // Process error details case "tr": HtmlNodeCollection errorChilds = child.SelectNodes(".//td//em | .//td//code[@class='input']"); foreach (HtmlNode errorChild in errorChilds) { switch (errorChild.Name.ToLowerCSafe()) { case "em": Match[] position = new Match[2]; IntNumberRegEx.Matches(errorChild.InnerText).CopyTo(position, 0); line = ValidationHelper.GetInteger(position[0].Value, 0); col = ValidationHelper.GetInteger(position[1].Value, 0); break; case "code": source = errorChild.InnerHtml; break; } } // Fill datarow content DataRow dr = tb.NewRow(); dr["accessibilityrule"] = mainRule + "
" + minorRule; dr["error"] = error; dr["fixsuggestion"] = fix; dr["line"] = line; dr["column"] = col; dr["source"] = source; // Add row to table tb.Rows.Add(dr); break; } } } } return tb; } /// /// General method to process validation and return validation results /// private DataSet ValidateHtml() { if (!String.IsNullOrEmpty(Url)) { string docHtml = GetHtml(Url); if (!String.IsNullOrEmpty(docHtml)) { DataSet dsValidationResult = GetValidationResult(docHtml); // Check if result contains error table if (!DataHelper.DataSourceIsEmpty(dsValidationResult) && !DataHelper.DataSourceIsEmpty(dsValidationResult.Tables["errors"])) { DataTable tbError = DocumentValidationHelper.ProcessValidationResult(dsValidationResult, DocumentValidationEnum.Accessibility, null); tbError.DefaultView.Sort = "line ASC"; DataSet result = new DataSet(); result.Tables.Add(tbError); return result; } else { return dsValidationResult; } } else { mErrorText = GetString("validation.diffdomainorprotocol"); } } return null; } /// /// Get dictionary with request parameters /// /// HTML data to be checked private Dictionary GetRequestDictionary(string data) { Dictionary reqData = new Dictionary(); reqData.Add("pastehtml", data); reqData.Add("validate_paste", "Check It"); reqData.Add("checkbox_gid[]", AccessibilityStandardCode.FromEnum(Standard).ToString()); reqData.Add("radio_gid[]", AccessibilityStandardCode.FromEnum(Standard).ToString()); reqData.Add("rpt_format", "1"); return reqData; } /// /// Get request data which will be sent using HTTP request to validator /// /// Data to create /// HTTP boundary string private string GetRequestData(Dictionary data, string boundary) { string separator = ResHelper.NewLine; boundary = "--" + boundary; // Prepare beginning of the request data StringBuilder sbRequest = new StringBuilder(); sbRequest.Append(boundary); sbRequest.Append(separator); // Process request form data foreach (string key in data.Keys) { sbRequest.Append(String.Format("Content-Disposition: form-data; name=\"{0}\"", key)); sbRequest.Append(separator); sbRequest.Append(separator); sbRequest.Append(data[key]); sbRequest.Append(separator); sbRequest.Append(boundary); sbRequest.Append(separator); } string request = sbRequest.ToString(); // Add final boundary dashes request = request.Insert(request.Length - 2, "--"); return request; } /// /// Process validation results /// /// DataSet with result of validation public void ProcessResult(DataSet validationResult) { if (validationResult != null) { pnlStatus.Visible = true; mErrorText = null; // Check if result is not empty if (!DataHelper.DataSourceIsEmpty(validationResult)) { // Show validation errors lblStatus.Text = GetString("validation.access.resultinvalid"); imgStatus.ImageUrl = GetImageUrl("Design/Controls/Validation/warning.png"); lblResults.Visible = true; lblResults.Text = ResHelper.GetString("validation.validationresults"); gridValidationResult.Visible = true; } else { // Show validation is valid lblStatus.Text = GetString("validation.access.resultvalid"); lblResults.Visible = false; gridValidationResult.Visible = false; imgStatus.ImageUrl = GetImageUrl("Design/Controls/Validation/check.png"); } } else { // No results obtained during validation, show error pnlStatus.Visible = false; lblResults.Visible = false; gridValidationResult.Visible = false; if (string.IsNullOrEmpty(mErrorText)) { mErrorText = GetString("validation.errorinitialization"); } } } #endregion }