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
}