using System; using System.Collections.Generic; using System.Data; using System.Collections; using System.Linq; using System.Net; using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Web; using System.Web.UI.WebControls; using CMS.CMSHelper; using CMS.DataEngine; using CMS.GlobalHelper; using CMS.IO; using CMS.SettingsProvider; using CMS.SiteProvider; using CMS.UIControls; using CMS.ExtendedControls.ActionsConfig; using CMS.ExtendedControls; public partial class CMSAdminControls_Validation_CssValidator : DocumentValidator { #region "Constants" private const string DEFAULT_VALIDATOR_URL = "http://jigsaw.w3.org/css-validator/validator"; private const string VALIDATOR_PROFILE = "css21"; private const int VALIDATION_DELAY = 300; #endregion #region "Variables" private string mValidatorURL = null; private string mValidatorProfile = null; private int mValidationDelay = 0; private bool mUseServerRequest = false; private string mErrorText = null; private string mInfoText = null; private Regex mInlineStylesRegex = null; private Regex mLinkedStylesRegex = null; private CurrentUserInfo currentUser = null; private SiteInfo currentSite = null; private string currentCulture = CultureHelper.DefaultUICulture; private static DataSet mDataSource = null; private static readonly Hashtable mPostProcessingRequired = new Hashtable(); private static readonly Hashtable mDataSources = new Hashtable(); private static readonly Hashtable mErrors = new Hashtable(); private string mExcludedCSS = ";designmode.css;skin.css;"; #endregion #region "Properties" /// /// URL to which validator requests will be sent /// public string ValidatorURL { get { return mValidatorURL ?? (mValidatorURL = DataHelper.GetNotEmpty(SettingsHelper.AppSettings["CMSValidationCSSValidatorURL"], DEFAULT_VALIDATOR_URL)); } set { mValidatorURL = value; } } /// /// Current log context /// public LogContext CurrentLog { get { return EnsureLog(); } } /// /// 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; } } /// /// Gets or sets source of the data for unigrid control /// public override DataSet DataSource { get { if (mDataSource == null) { mDataSource = base.DataSource ?? mDataSources[ctlAsync.ProcessGUID] as DataSet; } base.DataSource = mDataSource; return mDataSource; } set { mDataSource = value; mDataSources[ctlAsync.ProcessGUID] = mDataSource; base.DataSource = mDataSource; } } /// /// Current Error /// private string CurrentError { get { return ValidationHelper.GetString(mErrors["LinkChecker_" + ctlAsync.ProcessGUID], string.Empty); } set { mErrors["LinkChecker_" + ctlAsync.ProcessGUID] = value; } } /// /// Regular expression to get inline css styles /// private Regex InlineStylesRegex { get { if (mInlineStylesRegex == null) { mInlineStylesRegex = RegexHelper.GetRegex("]*>(?)", RegexOptions.Singleline); } return mInlineStylesRegex; } } /// /// Regular expression to get linked css styles /// private Regex LinkedStylesRegex { get { if (mLinkedStylesRegex == null) { mLinkedStylesRegex = RegexHelper.GetRegex("(?]*(?type\\s*=\\s*(?[\"']?)text/css(?(qc1)\\k))?[^>]*href\\s*=\\s*(?[\"'])|@import\\s*url\\s*(?\\()?(?[\"'])?(?=([^<])*(?(link)[^\"'>\\s]*|[^\"']*))(?(link)(?(qc2)\\k)[^>]*(?(type)|(\\s*type\\s*=\\s*(?[\"']?)text/css(?(qc1)\\k))))", RegexOptions.Singleline); } return mLinkedStylesRegex; } } /// /// Key to store validation result /// protected override string ResultKey { get { return "validation|css|" + CultureCode + "|" + Url; } } /// /// Indicates which CSS profile should be used for validation /// private string ValidatorProfile { get { return mValidatorProfile; } } /// /// Delay between validation requests to server /// private int ValidationDelay { get { if (mValidationDelay == 0) { mValidationDelay = ValidationHelper.GetInteger(SettingsHelper.AppSettings["CMSValidationCSSValidatorDelay"], VALIDATION_DELAY); } return mValidationDelay; } } /// /// Indicates if data post processing required /// private bool DataPostProcessing { get { return ValidationHelper.GetBoolean(mPostProcessingRequired[ctlAsync.ProcessGUID], false); } set { mPostProcessingRequired[ctlAsync.ProcessGUID] = value; } } /// /// Messages placeholder /// public override MessagesPlaceHolder MessagesPlaceHolder { get { return plcMess; } } #endregion #region "Control methods" /// /// Page load /// protected void Page_Load(object sender, EventArgs e) { if (!RequestHelper.IsPostBack()) { DataSource = null; } // Configure controls SetupControls(); if (RequestHelper.IsPostBack()) { ProcessResult(DataSource); } } /// /// Page PreRender /// protected void Page_PreRender(object sender, EventArgs e) { if (!string.IsNullOrEmpty(mErrorText)) { ShowError(mErrorText); } if (!string.IsNullOrEmpty(mInfoText)) { ShowInformation(mInfoText); } } /// /// Initializes all nested controls. /// private void SetupControls() { IsLiveSite = false; InitializeScripts(); // Set current UI culture currentCulture = CultureHelper.PreferredUICulture; // Initialize current user currentUser = CMSContext.CurrentUser; // Initialize current site currentSite = CMSContext.CurrentSite; // Initialize events ctlAsync.OnFinished += ctlAsync_OnFinished; ctlAsync.OnError += ctlAsync_OnError; ctlAsync.OnRequestLog += ctlAsync_OnRequestLog; ctlAsync.OnCancel += ctlAsync_OnCancel; ctlAsync.PostbackOnError = true; // Initialize cancel button btnCancel.Text = ResHelper.GetString("general.cancel"); btnCancel.Attributes.Add("onclick", ctlAsync.GetCancelScript(true) + "return false;"); titleElemAsync.TitleText = GetString("validation.css.checkingcss"); titleElemAsync.TitleImage = GetImageUrl("Design/Controls/Validation/check.png"); HeaderActions.ActionsList.Clear(); // Validate action HeaderAction validate = new HeaderAction(); validate.ControlType = HeaderActionTypeEnum.Hyperlink; 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 { 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"); } // Add actions and set help topic HeaderActions.AddAction(validate); HeaderActions.AddAction(viewCode); HeaderActions.AddAction(newWindow); HeaderActions.HelpName = "helpTopic"; HeaderActions.HelpTopicName = "cssvalidator"; HeaderActions.ActionPerformed += HeaderActions_ActionPerformed; // Set sorting and add events gridValidationResult.OrderBy = "line"; gridValidationResult.IsLiveSite = IsLiveSite; gridValidationResult.ZeroRowsText = GetString("validation.css.notvalidated"); gridValidationResult.OnExternalDataBound += gridValidationResult_OnExternalDataBound; gridValidationResult.OnDataReload += gridValidationResult_OnDataReload; gridValidationResult.ShowActionsMenu = true; gridValidationResult.AllColumns = "line, context, message, source"; } /// /// Actions handler. /// protected void HeaderActions_ActionPerformed(object sender, CommandEventArgs e) { switch (e.CommandName) { case "validate": Validate(); break; } } protected DataSet gridValidationResult_OnDataReload(string completeWhere, string currentOrder, int currentTopN, string columns, int currentOffset, int currentPageSize, ref int totalRecords) { DataSet ds = null; if (!DataHelper.DataSourceIsEmpty(DataSource)) { if (DataPostProcessing) { ds = DocumentValidationHelper.PostProcessValidationData(DataSource, DocumentValidationEnum.CSS, null); DataPostProcessing = false; } else { ds = DataSource; } } return ds; } /// /// Actions handler. /// private void Validate() { pnlLog.Visible = true; DataSource = null; pnlGrid.Visible = false; CurrentLog.Close(); CurrentError = string.Empty; EnsureLog(); // Get the full domain ctlAsync.Parameter = URLHelper.GetFullDomain() + ";" + URLHelper.GetFullApplicationUrl() + ";" + URLHelper.RemoveProtocolAndDomain(Url); ctlAsync.RunAsync(CheckCss, WindowsIdentity.GetCurrent()); } /// /// On external databound 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" /// /// Prepare Dictionary with requests for CSS validation /// /// Asynchronous parameter containing current url data to resolve absolute URL private Dictionary GetValidationRequests(string parameter) { string html = GetHtml(Url); Dictionary cssRequests = null; string[] urlParams = parameter.Split(';'); if (!String.IsNullOrEmpty(html)) { cssRequests = new Dictionary(); // Get inline CSS AddLog(GetString("validation.css.preparinginline")); StringBuilder sbInline = new StringBuilder(); foreach (Match m in InlineStylesRegex.Matches(html)) { string captured = m.Groups["css"].Value; sbInline.Append(captured); sbInline.Append("\n"); } cssRequests.Add(DocumentValidationHelper.InlineCSSSource, sbInline.ToString()); // Get linked styles URLs WebClient client = new WebClient(); foreach (Match m in LinkedStylesRegex.Matches(html)) { string url = m.Groups["url"].Value; url = Server.HtmlDecode(url); string css = null; if (!String.IsNullOrEmpty(url)) { bool processCss = true; string[] excludedCsss = mExcludedCSS.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); // Check if CSS is not excluded (CMS stylesheets) foreach (string excludedCss in excludedCsss) { if (url.EndsWithCSafe(excludedCss, true)) { processCss = false; break; } } if (processCss && !cssRequests.ContainsKey(url)) { AddLog(String.Format(GetString("validation.css.preparinglinkedstyles"), url)); try { // Get CSS data from URL string readUrl = DocumentValidationHelper.DisableMinificationOnUrl(URLHelper.GetAbsoluteUrl(url, urlParams[0], urlParams[1], urlParams[2])); StreamReader reader = StreamReader.New(client.OpenRead(readUrl)); css = reader.ReadToEnd(); if (!String.IsNullOrEmpty(css)) { cssRequests.Add(url, css.Trim(new char[] { '\r', '\n' })); } } catch { } } } } } return cssRequests; } /// /// 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 /// /// Validator parameters /// Parameter /// DataSet containing validator response private DataSet GetValidationResults(Dictionary validationData, string parameter) { DataSet dsResponse = null; List validatedUrls = validationData.Keys.ToList(); Random randGen = new Random(); DataSet dsResult = DataSource = ((validationData.Count == 1) && string.IsNullOrEmpty(validationData[validatedUrls[0]])) ? new DataSet() : null ; DataTable dtResponse = null; HttpWebResponse webResponse = null; string source = null; int counter = 0; while (validatedUrls.Count > 0) { // Check if source is processed repeatedly if (source == validatedUrls[0]) { counter++; } else { counter = 0; } // Set current source to validate source = validatedUrls[0]; string cssData = validationData[source]; validatedUrls.RemoveAt(0); if (!String.IsNullOrEmpty(cssData)) { // Create web request HttpWebRequest req = (HttpWebRequest)WebRequest.Create(ValidatorURL); req.Method = "POST"; req.ReadWriteTimeout = req.Timeout = 10000; 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(cssData), boundary)); req.ContentLength = data.Length; StreamWrapper writer = StreamWrapper.New(req.GetRequestStream()); writer.Write(data, 0, data.Length); writer.Close(); try { // Process server answer AddLog(String.Format(GetString("validation.css.validatingcss"), source)); using (webResponse = (HttpWebResponse)req.GetResponse()) { using (StreamWrapper response = StreamWrapper.New(webResponse.GetResponseStream())) { if (response != null) { if (dsResult == null) { dsResult = DataSource = new DataSet(); } dsResponse = new DataSet(); dsResponse.ReadXml(response.SystemStream); response.Close(); } } webResponse.Close(); } string[] currentUrlValues = parameter.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); Dictionary parameters = new Dictionary(); parameters["sitename"] = CMSContext.CurrentSiteName; parameters["user"] = currentUser; parameters["source"] = source; parameters["domainurl"] = currentUrlValues[0]; parameters["applicationurl"] = currentUrlValues[1]; dtResponse = DocumentValidationHelper.ProcessValidationResult(dsResponse, DocumentValidationEnum.CSS, parameters); // Check if response contain any relevant data if (!DataHelper.DataSourceIsEmpty(dtResponse)) { // Add response data to validation DataSet if (DataHelper.DataSourceIsEmpty(dsResult)) { dsResult.Tables.Add(dtResponse); } else { dsResult.Tables[0].Merge(dtResponse); } } } catch { if (counter < 5) { validatedUrls.Insert(0, source); } else { AddError(string.Format(GetString("validation.css.cssnotvalidated"), source)); } } finally { req.Abort(); Thread.Sleep(ValidationDelay); } } } return dsResult; } /// /// Get dictionary with request parameters /// /// CSS data to be checked private Dictionary GetRequestDictionary(string data) { Dictionary reqData = new Dictionary(); reqData.Add("text", data); reqData.Add("profile", ValidatorProfile); reqData.Add("usermedium", "all"); reqData.Add("type", "none"); reqData.Add("warning", "1"); reqData.Add("output", "soap12"); 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 = "\r\n"; 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.css.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.css.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"); } } } /// /// Check document CSS /// /// Parameter containing data to resolve relative links to absolute private void CheckCss(object parameter) { try { AddLog(ResHelper.GetString("validation.css.checkingcss", currentCulture)); Dictionary requests = GetValidationRequests(ValidationHelper.GetString(parameter, null)); // Ensure thread doesn't finish to early in special situations if ((requests == null) || (requests.Count <= 1)) { Thread.Sleep(200); } if (requests != null) { GetValidationResults(requests, ValidationHelper.GetString(parameter, null)); DataPostProcessing = true; } else { CurrentError = GetString("validation.diffdomainorprotocol"); } pnlLog.Visible = false; } catch (ThreadAbortException ex) { string state = ValidationHelper.GetString(ex.ExceptionState, string.Empty); if (state == CMSThread.ABORT_REASON_STOP) { // When canceled AddLog(ResHelper.GetString("validation.css.abort", currentCulture)); ctlAsync.RaiseError(null, null); } else { mErrorText = ex.Message; } } catch (Exception ex) { mErrorText = ex.Message; } } #endregion #region "Handling async thread" /// /// On cancel event /// private void ctlAsync_OnCancel(object sender, EventArgs e) { ctlAsync.Parameter = null; AddError(ResHelper.GetString("validation.validationcanceled")); ScriptHelper.RegisterStartupScript(this, typeof(string), "CancelLog", ScriptHelper.GetScript("var __pendingCallbacks = new Array();")); string separator = "
"; int error = CurrentError.IndexOf(separator); mInfoText = CurrentError.Substring(0, error); mErrorText = CurrentError.Substring(error + separator.Length); pnlLog.Visible = false; pnlGrid.Visible = true; CurrentLog.Close(); DataPostProcessing = true; } /// /// On request log event /// private void ctlAsync_OnRequestLog(object sender, EventArgs e) { ctlAsync.Log = CurrentLog.Log; } /// /// On error event /// private void ctlAsync_OnError(object sender, EventArgs e) { if (ctlAsync.Status == AsyncWorkerStatusEnum.Running) { ctlAsync.Stop(); } ctlAsync.Parameter = null; if (!string.IsNullOrEmpty(CurrentError)) { mErrorText = CurrentError; } CurrentLog.Close(); } /// /// On finished event /// private void ctlAsync_OnFinished(object sender, EventArgs e) { if (!string.IsNullOrEmpty(CurrentError)) { mErrorText = CurrentError; } CurrentLog.Close(); pnlLog.Visible = false; pnlGrid.Visible = true; } /// /// Ensures the logging context /// protected LogContext EnsureLog() { LogContext log = LogContext.EnsureLog(ctlAsync.ProcessGUID); log.Reversed = true; log.LineSeparator = "
"; return log; } /// /// Adds the log information /// /// New log information protected void AddLog(string newLog) { EnsureLog(); LogContext.AppendLine(newLog); } /// /// Adds the error to collection of errors /// /// Error message protected void AddError(string error) { AddLog(error); CurrentError = (error + "
" + CurrentError); } #endregion }